]> git.madduck.net Git - etc/vim.git/blob - src/black/__init__.py

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

All patches and comments are welcome. Please squash your changes to logical commits before using git-format-patch and git-send-email to patches@git.madduck.net. If you'd read over the Git project's submission guidelines and adhered to them, I'd be especially grateful.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

Remove hacky subprocess call in action.yml (#3226)
[etc/vim.git] / src / black / __init__.py
1 import asyncio
2 import io
3 import json
4 import os
5 import platform
6 import re
7 import signal
8 import sys
9 import tokenize
10 import traceback
11 from contextlib import contextmanager
12 from dataclasses import replace
13 from datetime import datetime
14 from enum import Enum
15 from json.decoder import JSONDecodeError
16 from multiprocessing import Manager, freeze_support
17 from pathlib import Path
18 from typing import (
19     TYPE_CHECKING,
20     Any,
21     Dict,
22     Generator,
23     Iterator,
24     List,
25     MutableMapping,
26     Optional,
27     Pattern,
28     Sequence,
29     Set,
30     Sized,
31     Tuple,
32     Union,
33 )
34
35 import click
36 from click.core import ParameterSource
37 from mypy_extensions import mypyc_attr
38 from pathspec.patterns.gitwildmatch import GitWildMatchPatternError
39
40 from _black_version import version as __version__
41 from black.cache import Cache, filter_cached, get_cache_info, read_cache, write_cache
42 from black.comments import normalize_fmt_off
43 from black.concurrency import cancel, maybe_install_uvloop, shutdown
44 from black.const import (
45     DEFAULT_EXCLUDES,
46     DEFAULT_INCLUDES,
47     DEFAULT_LINE_LENGTH,
48     STDIN_PLACEHOLDER,
49 )
50 from black.files import (
51     find_project_root,
52     find_pyproject_toml,
53     find_user_pyproject_toml,
54     gen_python_files,
55     get_gitignore,
56     normalize_path_maybe_ignore,
57     parse_pyproject_toml,
58     wrap_stream_for_windows,
59 )
60 from black.handle_ipynb_magics import (
61     PYTHON_CELL_MAGICS,
62     TRANSFORMED_MAGICS,
63     jupyter_dependencies_are_installed,
64     mask_cell,
65     put_trailing_semicolon_back,
66     remove_trailing_semicolon,
67     unmask_cell,
68 )
69 from black.linegen import LN, LineGenerator, transform_line
70 from black.lines import EmptyLineTracker, Line
71 from black.mode import (
72     FUTURE_FLAG_TO_FEATURE,
73     VERSION_TO_FEATURES,
74     Feature,
75     Mode,
76     TargetVersion,
77     supports_feature,
78 )
79 from black.nodes import (
80     STARS,
81     is_number_token,
82     is_simple_decorator_expression,
83     is_string_token,
84     syms,
85 )
86 from black.output import color_diff, diff, dump_to_file, err, ipynb_diff, out
87 from black.parsing import InvalidInput  # noqa F401
88 from black.parsing import lib2to3_parse, parse_ast, stringify_ast
89 from black.report import Changed, NothingChanged, Report
90 from black.trans import iter_fexpr_spans
91 from blib2to3.pgen2 import token
92 from blib2to3.pytree import Leaf, Node
93
94 if TYPE_CHECKING:
95     from concurrent.futures import Executor
96
97 COMPILED = Path(__file__).suffix in (".pyd", ".so")
98
99 # types
100 FileContent = str
101 Encoding = str
102 NewLine = str
103
104
105 class WriteBack(Enum):
106     NO = 0
107     YES = 1
108     DIFF = 2
109     CHECK = 3
110     COLOR_DIFF = 4
111
112     @classmethod
113     def from_configuration(
114         cls, *, check: bool, diff: bool, color: bool = False
115     ) -> "WriteBack":
116         if check and not diff:
117             return cls.CHECK
118
119         if diff and color:
120             return cls.COLOR_DIFF
121
122         return cls.DIFF if diff else cls.YES
123
124
125 # Legacy name, left for integrations.
126 FileMode = Mode
127
128 DEFAULT_WORKERS = os.cpu_count()
129
130
131 def read_pyproject_toml(
132     ctx: click.Context, param: click.Parameter, value: Optional[str]
133 ) -> Optional[str]:
134     """Inject Black configuration from "pyproject.toml" into defaults in `ctx`.
135
136     Returns the path to a successfully found and read configuration file, None
137     otherwise.
138     """
139     if not value:
140         value = find_pyproject_toml(ctx.params.get("src", ()))
141         if value is None:
142             return None
143
144     try:
145         config = parse_pyproject_toml(value)
146     except (OSError, ValueError) as e:
147         raise click.FileError(
148             filename=value, hint=f"Error reading configuration file: {e}"
149         ) from None
150
151     if not config:
152         return None
153     else:
154         # Sanitize the values to be Click friendly. For more information please see:
155         # https://github.com/psf/black/issues/1458
156         # https://github.com/pallets/click/issues/1567
157         config = {
158             k: str(v) if not isinstance(v, (list, dict)) else v
159             for k, v in config.items()
160         }
161
162     target_version = config.get("target_version")
163     if target_version is not None and not isinstance(target_version, list):
164         raise click.BadOptionUsage(
165             "target-version", "Config key target-version must be a list"
166         )
167
168     default_map: Dict[str, Any] = {}
169     if ctx.default_map:
170         default_map.update(ctx.default_map)
171     default_map.update(config)
172
173     ctx.default_map = default_map
174     return value
175
176
177 def target_version_option_callback(
178     c: click.Context, p: Union[click.Option, click.Parameter], v: Tuple[str, ...]
179 ) -> List[TargetVersion]:
180     """Compute the target versions from a --target-version flag.
181
182     This is its own function because mypy couldn't infer the type correctly
183     when it was a lambda, causing mypyc trouble.
184     """
185     return [TargetVersion[val.upper()] for val in v]
186
187
188 def re_compile_maybe_verbose(regex: str) -> Pattern[str]:
189     """Compile a regular expression string in `regex`.
190
191     If it contains newlines, use verbose mode.
192     """
193     if "\n" in regex:
194         regex = "(?x)" + regex
195     compiled: Pattern[str] = re.compile(regex)
196     return compiled
197
198
199 def validate_regex(
200     ctx: click.Context,
201     param: click.Parameter,
202     value: Optional[str],
203 ) -> Optional[Pattern[str]]:
204     try:
205         return re_compile_maybe_verbose(value) if value is not None else None
206     except re.error as e:
207         raise click.BadParameter(f"Not a valid regular expression: {e}") from None
208
209
210 @click.command(
211     context_settings={"help_option_names": ["-h", "--help"]},
212     # While Click does set this field automatically using the docstring, mypyc
213     # (annoyingly) strips 'em so we need to set it here too.
214     help="The uncompromising code formatter.",
215 )
216 @click.option("-c", "--code", type=str, help="Format the code passed in as a string.")
217 @click.option(
218     "-l",
219     "--line-length",
220     type=int,
221     default=DEFAULT_LINE_LENGTH,
222     help="How many characters per line to allow.",
223     show_default=True,
224 )
225 @click.option(
226     "-t",
227     "--target-version",
228     type=click.Choice([v.name.lower() for v in TargetVersion]),
229     callback=target_version_option_callback,
230     multiple=True,
231     help=(
232         "Python versions that should be supported by Black's output. [default: per-file"
233         " auto-detection]"
234     ),
235 )
236 @click.option(
237     "--pyi",
238     is_flag=True,
239     help=(
240         "Format all input files like typing stubs regardless of file extension (useful"
241         " when piping source on standard input)."
242     ),
243 )
244 @click.option(
245     "--ipynb",
246     is_flag=True,
247     help=(
248         "Format all input files like Jupyter Notebooks regardless of file extension "
249         "(useful when piping source on standard input)."
250     ),
251 )
252 @click.option(
253     "--python-cell-magics",
254     multiple=True,
255     help=(
256         "When processing Jupyter Notebooks, add the given magic to the list"
257         f" of known python-magics ({', '.join(PYTHON_CELL_MAGICS)})."
258         " Useful for formatting cells with custom python magics."
259     ),
260     default=[],
261 )
262 @click.option(
263     "-S",
264     "--skip-string-normalization",
265     is_flag=True,
266     help="Don't normalize string quotes or prefixes.",
267 )
268 @click.option(
269     "-C",
270     "--skip-magic-trailing-comma",
271     is_flag=True,
272     help="Don't use trailing commas as a reason to split lines.",
273 )
274 @click.option(
275     "--experimental-string-processing",
276     is_flag=True,
277     hidden=True,
278     help="(DEPRECATED and now included in --preview) Normalize string literals.",
279 )
280 @click.option(
281     "--preview",
282     is_flag=True,
283     help=(
284         "Enable potentially disruptive style changes that may be added to Black's main"
285         " functionality in the next major release."
286     ),
287 )
288 @click.option(
289     "--check",
290     is_flag=True,
291     help=(
292         "Don't write the files back, just return the status. Return code 0 means"
293         " nothing would change. Return code 1 means some files would be reformatted."
294         " Return code 123 means there was an internal error."
295     ),
296 )
297 @click.option(
298     "--diff",
299     is_flag=True,
300     help="Don't write the files back, just output a diff for each file on stdout.",
301 )
302 @click.option(
303     "--color/--no-color",
304     is_flag=True,
305     help="Show colored diff. Only applies when `--diff` is given.",
306 )
307 @click.option(
308     "--fast/--safe",
309     is_flag=True,
310     help="If --fast given, skip temporary sanity checks. [default: --safe]",
311 )
312 @click.option(
313     "--required-version",
314     type=str,
315     help=(
316         "Require a specific version of Black to be running (useful for unifying results"
317         " across many environments e.g. with a pyproject.toml file). It can be"
318         " either a major version number or an exact version."
319     ),
320 )
321 @click.option(
322     "--include",
323     type=str,
324     default=DEFAULT_INCLUDES,
325     callback=validate_regex,
326     help=(
327         "A regular expression that matches files and directories that should be"
328         " included on recursive searches. An empty value means all files are included"
329         " regardless of the name. Use forward slashes for directories on all platforms"
330         " (Windows, too). Exclusions are calculated first, inclusions later."
331     ),
332     show_default=True,
333 )
334 @click.option(
335     "--exclude",
336     type=str,
337     callback=validate_regex,
338     help=(
339         "A regular expression that matches files and directories that should be"
340         " excluded on recursive searches. An empty value means no paths are excluded."
341         " Use forward slashes for directories on all platforms (Windows, too)."
342         " Exclusions are calculated first, inclusions later. [default:"
343         f" {DEFAULT_EXCLUDES}]"
344     ),
345     show_default=False,
346 )
347 @click.option(
348     "--extend-exclude",
349     type=str,
350     callback=validate_regex,
351     help=(
352         "Like --exclude, but adds additional files and directories on top of the"
353         " excluded ones. (Useful if you simply want to add to the default)"
354     ),
355 )
356 @click.option(
357     "--force-exclude",
358     type=str,
359     callback=validate_regex,
360     help=(
361         "Like --exclude, but files and directories matching this regex will be "
362         "excluded even when they are passed explicitly as arguments."
363     ),
364 )
365 @click.option(
366     "--stdin-filename",
367     type=str,
368     help=(
369         "The name of the file when passing it through stdin. Useful to make "
370         "sure Black will respect --force-exclude option on some "
371         "editors that rely on using stdin."
372     ),
373 )
374 @click.option(
375     "-W",
376     "--workers",
377     type=click.IntRange(min=1),
378     default=DEFAULT_WORKERS,
379     show_default=True,
380     help="Number of parallel workers",
381 )
382 @click.option(
383     "-q",
384     "--quiet",
385     is_flag=True,
386     help=(
387         "Don't emit non-error messages to stderr. Errors are still emitted; silence"
388         " those with 2>/dev/null."
389     ),
390 )
391 @click.option(
392     "-v",
393     "--verbose",
394     is_flag=True,
395     help=(
396         "Also emit messages to stderr about files that were not changed or were ignored"
397         " due to exclusion patterns."
398     ),
399 )
400 @click.version_option(
401     version=__version__,
402     message=(
403         f"%(prog)s, %(version)s (compiled: {'yes' if COMPILED else 'no'})\n"
404         f"Python ({platform.python_implementation()}) {platform.python_version()}"
405     ),
406 )
407 @click.argument(
408     "src",
409     nargs=-1,
410     type=click.Path(
411         exists=True, file_okay=True, dir_okay=True, readable=True, allow_dash=True
412     ),
413     is_eager=True,
414     metavar="SRC ...",
415 )
416 @click.option(
417     "--config",
418     type=click.Path(
419         exists=True,
420         file_okay=True,
421         dir_okay=False,
422         readable=True,
423         allow_dash=False,
424         path_type=str,
425     ),
426     is_eager=True,
427     callback=read_pyproject_toml,
428     help="Read configuration from FILE path.",
429 )
430 @click.pass_context
431 def main(  # noqa: C901
432     ctx: click.Context,
433     code: Optional[str],
434     line_length: int,
435     target_version: List[TargetVersion],
436     check: bool,
437     diff: bool,
438     color: bool,
439     fast: bool,
440     pyi: bool,
441     ipynb: bool,
442     python_cell_magics: Sequence[str],
443     skip_string_normalization: bool,
444     skip_magic_trailing_comma: bool,
445     experimental_string_processing: bool,
446     preview: bool,
447     quiet: bool,
448     verbose: bool,
449     required_version: Optional[str],
450     include: Pattern[str],
451     exclude: Optional[Pattern[str]],
452     extend_exclude: Optional[Pattern[str]],
453     force_exclude: Optional[Pattern[str]],
454     stdin_filename: Optional[str],
455     workers: int,
456     src: Tuple[str, ...],
457     config: Optional[str],
458 ) -> None:
459     """The uncompromising code formatter."""
460     ctx.ensure_object(dict)
461
462     if src and code is not None:
463         out(
464             main.get_usage(ctx)
465             + "\n\n'SRC' and 'code' cannot be passed simultaneously."
466         )
467         ctx.exit(1)
468     if not src and code is None:
469         out(main.get_usage(ctx) + "\n\nOne of 'SRC' or 'code' is required.")
470         ctx.exit(1)
471
472     root, method = find_project_root(src) if code is None else (None, None)
473     ctx.obj["root"] = root
474
475     if verbose:
476         if root:
477             out(
478                 f"Identified `{root}` as project root containing a {method}.",
479                 fg="blue",
480             )
481
482             normalized = [
483                 (normalize_path_maybe_ignore(Path(source), root), source)
484                 for source in src
485             ]
486             srcs_string = ", ".join(
487                 [
488                     f'"{_norm}"'
489                     if _norm
490                     else f'\033[31m"{source} (skipping - invalid)"\033[34m'
491                     for _norm, source in normalized
492                 ]
493             )
494             out(f"Sources to be formatted: {srcs_string}", fg="blue")
495
496         if config:
497             config_source = ctx.get_parameter_source("config")
498             user_level_config = str(find_user_pyproject_toml())
499             if config == user_level_config:
500                 out(
501                     "Using configuration from user-level config at "
502                     f"'{user_level_config}'.",
503                     fg="blue",
504                 )
505             elif config_source in (
506                 ParameterSource.DEFAULT,
507                 ParameterSource.DEFAULT_MAP,
508             ):
509                 out("Using configuration from project root.", fg="blue")
510             else:
511                 out(f"Using configuration in '{config}'.", fg="blue")
512
513     error_msg = "Oh no! 💥 💔 💥"
514     if (
515         required_version
516         and required_version != __version__
517         and required_version != __version__.split(".")[0]
518     ):
519         err(
520             f"{error_msg} The required version `{required_version}` does not match"
521             f" the running version `{__version__}`!"
522         )
523         ctx.exit(1)
524     if ipynb and pyi:
525         err("Cannot pass both `pyi` and `ipynb` flags!")
526         ctx.exit(1)
527
528     write_back = WriteBack.from_configuration(check=check, diff=diff, color=color)
529     if target_version:
530         versions = set(target_version)
531     else:
532         # We'll autodetect later.
533         versions = set()
534     mode = Mode(
535         target_versions=versions,
536         line_length=line_length,
537         is_pyi=pyi,
538         is_ipynb=ipynb,
539         string_normalization=not skip_string_normalization,
540         magic_trailing_comma=not skip_magic_trailing_comma,
541         experimental_string_processing=experimental_string_processing,
542         preview=preview,
543         python_cell_magics=set(python_cell_magics),
544     )
545
546     if code is not None:
547         # Run in quiet mode by default with -c; the extra output isn't useful.
548         # You can still pass -v to get verbose output.
549         quiet = True
550
551     report = Report(check=check, diff=diff, quiet=quiet, verbose=verbose)
552
553     if code is not None:
554         reformat_code(
555             content=code, fast=fast, write_back=write_back, mode=mode, report=report
556         )
557     else:
558         try:
559             sources = get_sources(
560                 ctx=ctx,
561                 src=src,
562                 quiet=quiet,
563                 verbose=verbose,
564                 include=include,
565                 exclude=exclude,
566                 extend_exclude=extend_exclude,
567                 force_exclude=force_exclude,
568                 report=report,
569                 stdin_filename=stdin_filename,
570             )
571         except GitWildMatchPatternError:
572             ctx.exit(1)
573
574         path_empty(
575             sources,
576             "No Python files are present to be formatted. Nothing to do 😴",
577             quiet,
578             verbose,
579             ctx,
580         )
581
582         if len(sources) == 1:
583             reformat_one(
584                 src=sources.pop(),
585                 fast=fast,
586                 write_back=write_back,
587                 mode=mode,
588                 report=report,
589             )
590         else:
591             reformat_many(
592                 sources=sources,
593                 fast=fast,
594                 write_back=write_back,
595                 mode=mode,
596                 report=report,
597                 workers=workers,
598             )
599
600     if verbose or not quiet:
601         if code is None and (verbose or report.change_count or report.failure_count):
602             out()
603         out(error_msg if report.return_code else "All done! ✨ 🍰 ✨")
604         if code is None:
605             click.echo(str(report), err=True)
606     ctx.exit(report.return_code)
607
608
609 def get_sources(
610     *,
611     ctx: click.Context,
612     src: Tuple[str, ...],
613     quiet: bool,
614     verbose: bool,
615     include: Pattern[str],
616     exclude: Optional[Pattern[str]],
617     extend_exclude: Optional[Pattern[str]],
618     force_exclude: Optional[Pattern[str]],
619     report: "Report",
620     stdin_filename: Optional[str],
621 ) -> Set[Path]:
622     """Compute the set of files to be formatted."""
623     sources: Set[Path] = set()
624
625     if exclude is None:
626         exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES)
627         gitignore = get_gitignore(ctx.obj["root"])
628     else:
629         gitignore = None
630
631     for s in src:
632         if s == "-" and stdin_filename:
633             p = Path(stdin_filename)
634             is_stdin = True
635         else:
636             p = Path(s)
637             is_stdin = False
638
639         if is_stdin or p.is_file():
640             normalized_path = normalize_path_maybe_ignore(p, ctx.obj["root"], report)
641             if normalized_path is None:
642                 continue
643
644             normalized_path = "/" + normalized_path
645             # Hard-exclude any files that matches the `--force-exclude` regex.
646             if force_exclude:
647                 force_exclude_match = force_exclude.search(normalized_path)
648             else:
649                 force_exclude_match = None
650             if force_exclude_match and force_exclude_match.group(0):
651                 report.path_ignored(p, "matches the --force-exclude regular expression")
652                 continue
653
654             if is_stdin:
655                 p = Path(f"{STDIN_PLACEHOLDER}{str(p)}")
656
657             if p.suffix == ".ipynb" and not jupyter_dependencies_are_installed(
658                 verbose=verbose, quiet=quiet
659             ):
660                 continue
661
662             sources.add(p)
663         elif p.is_dir():
664             sources.update(
665                 gen_python_files(
666                     p.iterdir(),
667                     ctx.obj["root"],
668                     include,
669                     exclude,
670                     extend_exclude,
671                     force_exclude,
672                     report,
673                     gitignore,
674                     verbose=verbose,
675                     quiet=quiet,
676                 )
677             )
678         elif s == "-":
679             sources.add(p)
680         else:
681             err(f"invalid path: {s}")
682     return sources
683
684
685 def path_empty(
686     src: Sized, msg: str, quiet: bool, verbose: bool, ctx: click.Context
687 ) -> None:
688     """
689     Exit if there is no `src` provided for formatting
690     """
691     if not src:
692         if verbose or not quiet:
693             out(msg)
694         ctx.exit(0)
695
696
697 def reformat_code(
698     content: str, fast: bool, write_back: WriteBack, mode: Mode, report: Report
699 ) -> None:
700     """
701     Reformat and print out `content` without spawning child processes.
702     Similar to `reformat_one`, but for string content.
703
704     `fast`, `write_back`, and `mode` options are passed to
705     :func:`format_file_in_place` or :func:`format_stdin_to_stdout`.
706     """
707     path = Path("<string>")
708     try:
709         changed = Changed.NO
710         if format_stdin_to_stdout(
711             content=content, fast=fast, write_back=write_back, mode=mode
712         ):
713             changed = Changed.YES
714         report.done(path, changed)
715     except Exception as exc:
716         if report.verbose:
717             traceback.print_exc()
718         report.failed(path, str(exc))
719
720
721 # diff-shades depends on being to monkeypatch this function to operate. I know it's
722 # not ideal, but this shouldn't cause any issues ... hopefully. ~ichard26
723 @mypyc_attr(patchable=True)
724 def reformat_one(
725     src: Path, fast: bool, write_back: WriteBack, mode: Mode, report: "Report"
726 ) -> None:
727     """Reformat a single file under `src` without spawning child processes.
728
729     `fast`, `write_back`, and `mode` options are passed to
730     :func:`format_file_in_place` or :func:`format_stdin_to_stdout`.
731     """
732     try:
733         changed = Changed.NO
734
735         if str(src) == "-":
736             is_stdin = True
737         elif str(src).startswith(STDIN_PLACEHOLDER):
738             is_stdin = True
739             # Use the original name again in case we want to print something
740             # to the user
741             src = Path(str(src)[len(STDIN_PLACEHOLDER) :])
742         else:
743             is_stdin = False
744
745         if is_stdin:
746             if src.suffix == ".pyi":
747                 mode = replace(mode, is_pyi=True)
748             elif src.suffix == ".ipynb":
749                 mode = replace(mode, is_ipynb=True)
750             if format_stdin_to_stdout(fast=fast, write_back=write_back, mode=mode):
751                 changed = Changed.YES
752         else:
753             cache: Cache = {}
754             if write_back not in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
755                 cache = read_cache(mode)
756                 res_src = src.resolve()
757                 res_src_s = str(res_src)
758                 if res_src_s in cache and cache[res_src_s] == get_cache_info(res_src):
759                     changed = Changed.CACHED
760             if changed is not Changed.CACHED and format_file_in_place(
761                 src, fast=fast, write_back=write_back, mode=mode
762             ):
763                 changed = Changed.YES
764             if (write_back is WriteBack.YES and changed is not Changed.CACHED) or (
765                 write_back is WriteBack.CHECK and changed is Changed.NO
766             ):
767                 write_cache(cache, [src], mode)
768         report.done(src, changed)
769     except Exception as exc:
770         if report.verbose:
771             traceback.print_exc()
772         report.failed(src, str(exc))
773
774
775 # diff-shades depends on being to monkeypatch this function to operate. I know it's
776 # not ideal, but this shouldn't cause any issues ... hopefully. ~ichard26
777 @mypyc_attr(patchable=True)
778 def reformat_many(
779     sources: Set[Path],
780     fast: bool,
781     write_back: WriteBack,
782     mode: Mode,
783     report: "Report",
784     workers: Optional[int],
785 ) -> None:
786     """Reformat multiple files using a ProcessPoolExecutor."""
787     from concurrent.futures import Executor, ProcessPoolExecutor, ThreadPoolExecutor
788
789     executor: Executor
790     worker_count = workers if workers is not None else DEFAULT_WORKERS
791     if sys.platform == "win32":
792         # Work around https://bugs.python.org/issue26903
793         assert worker_count is not None
794         worker_count = min(worker_count, 60)
795     try:
796         executor = ProcessPoolExecutor(max_workers=worker_count)
797     except (ImportError, NotImplementedError, OSError):
798         # we arrive here if the underlying system does not support multi-processing
799         # like in AWS Lambda or Termux, in which case we gracefully fallback to
800         # a ThreadPoolExecutor with just a single worker (more workers would not do us
801         # any good due to the Global Interpreter Lock)
802         executor = ThreadPoolExecutor(max_workers=1)
803
804     loop = asyncio.new_event_loop()
805     asyncio.set_event_loop(loop)
806     try:
807         loop.run_until_complete(
808             schedule_formatting(
809                 sources=sources,
810                 fast=fast,
811                 write_back=write_back,
812                 mode=mode,
813                 report=report,
814                 loop=loop,
815                 executor=executor,
816             )
817         )
818     finally:
819         try:
820             shutdown(loop)
821         finally:
822             asyncio.set_event_loop(None)
823         if executor is not None:
824             executor.shutdown()
825
826
827 async def schedule_formatting(
828     sources: Set[Path],
829     fast: bool,
830     write_back: WriteBack,
831     mode: Mode,
832     report: "Report",
833     loop: asyncio.AbstractEventLoop,
834     executor: "Executor",
835 ) -> None:
836     """Run formatting of `sources` in parallel using the provided `executor`.
837
838     (Use ProcessPoolExecutors for actual parallelism.)
839
840     `write_back`, `fast`, and `mode` options are passed to
841     :func:`format_file_in_place`.
842     """
843     cache: Cache = {}
844     if write_back not in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
845         cache = read_cache(mode)
846         sources, cached = filter_cached(cache, sources)
847         for src in sorted(cached):
848             report.done(src, Changed.CACHED)
849     if not sources:
850         return
851
852     cancelled = []
853     sources_to_cache = []
854     lock = None
855     if write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
856         # For diff output, we need locks to ensure we don't interleave output
857         # from different processes.
858         manager = Manager()
859         lock = manager.Lock()
860     tasks = {
861         asyncio.ensure_future(
862             loop.run_in_executor(
863                 executor, format_file_in_place, src, fast, mode, write_back, lock
864             )
865         ): src
866         for src in sorted(sources)
867     }
868     pending = tasks.keys()
869     try:
870         loop.add_signal_handler(signal.SIGINT, cancel, pending)
871         loop.add_signal_handler(signal.SIGTERM, cancel, pending)
872     except NotImplementedError:
873         # There are no good alternatives for these on Windows.
874         pass
875     while pending:
876         done, _ = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)
877         for task in done:
878             src = tasks.pop(task)
879             if task.cancelled():
880                 cancelled.append(task)
881             elif task.exception():
882                 report.failed(src, str(task.exception()))
883             else:
884                 changed = Changed.YES if task.result() else Changed.NO
885                 # If the file was written back or was successfully checked as
886                 # well-formatted, store this information in the cache.
887                 if write_back is WriteBack.YES or (
888                     write_back is WriteBack.CHECK and changed is Changed.NO
889                 ):
890                     sources_to_cache.append(src)
891                 report.done(src, changed)
892     if cancelled:
893         if sys.version_info >= (3, 7):
894             await asyncio.gather(*cancelled, return_exceptions=True)
895         else:
896             await asyncio.gather(*cancelled, loop=loop, return_exceptions=True)
897     if sources_to_cache:
898         write_cache(cache, sources_to_cache, mode)
899
900
901 def format_file_in_place(
902     src: Path,
903     fast: bool,
904     mode: Mode,
905     write_back: WriteBack = WriteBack.NO,
906     lock: Any = None,  # multiprocessing.Manager().Lock() is some crazy proxy
907 ) -> bool:
908     """Format file under `src` path. Return True if changed.
909
910     If `write_back` is DIFF, write a diff to stdout. If it is YES, write reformatted
911     code to the file.
912     `mode` and `fast` options are passed to :func:`format_file_contents`.
913     """
914     if src.suffix == ".pyi":
915         mode = replace(mode, is_pyi=True)
916     elif src.suffix == ".ipynb":
917         mode = replace(mode, is_ipynb=True)
918
919     then = datetime.utcfromtimestamp(src.stat().st_mtime)
920     with open(src, "rb") as buf:
921         src_contents, encoding, newline = decode_bytes(buf.read())
922     try:
923         dst_contents = format_file_contents(src_contents, fast=fast, mode=mode)
924     except NothingChanged:
925         return False
926     except JSONDecodeError:
927         raise ValueError(
928             f"File '{src}' cannot be parsed as valid Jupyter notebook."
929         ) from None
930
931     if write_back == WriteBack.YES:
932         with open(src, "w", encoding=encoding, newline=newline) as f:
933             f.write(dst_contents)
934     elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
935         now = datetime.utcnow()
936         src_name = f"{src}\t{then} +0000"
937         dst_name = f"{src}\t{now} +0000"
938         if mode.is_ipynb:
939             diff_contents = ipynb_diff(src_contents, dst_contents, src_name, dst_name)
940         else:
941             diff_contents = diff(src_contents, dst_contents, src_name, dst_name)
942
943         if write_back == WriteBack.COLOR_DIFF:
944             diff_contents = color_diff(diff_contents)
945
946         with lock or nullcontext():
947             f = io.TextIOWrapper(
948                 sys.stdout.buffer,
949                 encoding=encoding,
950                 newline=newline,
951                 write_through=True,
952             )
953             f = wrap_stream_for_windows(f)
954             f.write(diff_contents)
955             f.detach()
956
957     return True
958
959
960 def format_stdin_to_stdout(
961     fast: bool,
962     *,
963     content: Optional[str] = None,
964     write_back: WriteBack = WriteBack.NO,
965     mode: Mode,
966 ) -> bool:
967     """Format file on stdin. Return True if changed.
968
969     If content is None, it's read from sys.stdin.
970
971     If `write_back` is YES, write reformatted code back to stdout. If it is DIFF,
972     write a diff to stdout. The `mode` argument is passed to
973     :func:`format_file_contents`.
974     """
975     then = datetime.utcnow()
976
977     if content is None:
978         src, encoding, newline = decode_bytes(sys.stdin.buffer.read())
979     else:
980         src, encoding, newline = content, "utf-8", ""
981
982     dst = src
983     try:
984         dst = format_file_contents(src, fast=fast, mode=mode)
985         return True
986
987     except NothingChanged:
988         return False
989
990     finally:
991         f = io.TextIOWrapper(
992             sys.stdout.buffer, encoding=encoding, newline=newline, write_through=True
993         )
994         if write_back == WriteBack.YES:
995             # Make sure there's a newline after the content
996             if dst and dst[-1] != "\n":
997                 dst += "\n"
998             f.write(dst)
999         elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
1000             now = datetime.utcnow()
1001             src_name = f"STDIN\t{then} +0000"
1002             dst_name = f"STDOUT\t{now} +0000"
1003             d = diff(src, dst, src_name, dst_name)
1004             if write_back == WriteBack.COLOR_DIFF:
1005                 d = color_diff(d)
1006                 f = wrap_stream_for_windows(f)
1007             f.write(d)
1008         f.detach()
1009
1010
1011 def check_stability_and_equivalence(
1012     src_contents: str, dst_contents: str, *, mode: Mode
1013 ) -> None:
1014     """Perform stability and equivalence checks.
1015
1016     Raise AssertionError if source and destination contents are not
1017     equivalent, or if a second pass of the formatter would format the
1018     content differently.
1019     """
1020     assert_equivalent(src_contents, dst_contents)
1021     assert_stable(src_contents, dst_contents, mode=mode)
1022
1023
1024 def format_file_contents(src_contents: str, *, fast: bool, mode: Mode) -> FileContent:
1025     """Reformat contents of a file and return new contents.
1026
1027     If `fast` is False, additionally confirm that the reformatted code is
1028     valid by calling :func:`assert_equivalent` and :func:`assert_stable` on it.
1029     `mode` is passed to :func:`format_str`.
1030     """
1031     if not src_contents.strip():
1032         raise NothingChanged
1033
1034     if mode.is_ipynb:
1035         dst_contents = format_ipynb_string(src_contents, fast=fast, mode=mode)
1036     else:
1037         dst_contents = format_str(src_contents, mode=mode)
1038     if src_contents == dst_contents:
1039         raise NothingChanged
1040
1041     if not fast and not mode.is_ipynb:
1042         # Jupyter notebooks will already have been checked above.
1043         check_stability_and_equivalence(src_contents, dst_contents, mode=mode)
1044     return dst_contents
1045
1046
1047 def validate_cell(src: str, mode: Mode) -> None:
1048     """Check that cell does not already contain TransformerManager transformations,
1049     or non-Python cell magics, which might cause tokenizer_rt to break because of
1050     indentations.
1051
1052     If a cell contains ``!ls``, then it'll be transformed to
1053     ``get_ipython().system('ls')``. However, if the cell originally contained
1054     ``get_ipython().system('ls')``, then it would get transformed in the same way:
1055
1056         >>> TransformerManager().transform_cell("get_ipython().system('ls')")
1057         "get_ipython().system('ls')\n"
1058         >>> TransformerManager().transform_cell("!ls")
1059         "get_ipython().system('ls')\n"
1060
1061     Due to the impossibility of safely roundtripping in such situations, cells
1062     containing transformed magics will be ignored.
1063     """
1064     if any(transformed_magic in src for transformed_magic in TRANSFORMED_MAGICS):
1065         raise NothingChanged
1066     if (
1067         src[:2] == "%%"
1068         and src.split()[0][2:] not in PYTHON_CELL_MAGICS | mode.python_cell_magics
1069     ):
1070         raise NothingChanged
1071
1072
1073 def format_cell(src: str, *, fast: bool, mode: Mode) -> str:
1074     """Format code in given cell of Jupyter notebook.
1075
1076     General idea is:
1077
1078       - if cell has trailing semicolon, remove it;
1079       - if cell has IPython magics, mask them;
1080       - format cell;
1081       - reinstate IPython magics;
1082       - reinstate trailing semicolon (if originally present);
1083       - strip trailing newlines.
1084
1085     Cells with syntax errors will not be processed, as they
1086     could potentially be automagics or multi-line magics, which
1087     are currently not supported.
1088     """
1089     validate_cell(src, mode)
1090     src_without_trailing_semicolon, has_trailing_semicolon = remove_trailing_semicolon(
1091         src
1092     )
1093     try:
1094         masked_src, replacements = mask_cell(src_without_trailing_semicolon)
1095     except SyntaxError:
1096         raise NothingChanged from None
1097     masked_dst = format_str(masked_src, mode=mode)
1098     if not fast:
1099         check_stability_and_equivalence(masked_src, masked_dst, mode=mode)
1100     dst_without_trailing_semicolon = unmask_cell(masked_dst, replacements)
1101     dst = put_trailing_semicolon_back(
1102         dst_without_trailing_semicolon, has_trailing_semicolon
1103     )
1104     dst = dst.rstrip("\n")
1105     if dst == src:
1106         raise NothingChanged from None
1107     return dst
1108
1109
1110 def validate_metadata(nb: MutableMapping[str, Any]) -> None:
1111     """If notebook is marked as non-Python, don't format it.
1112
1113     All notebook metadata fields are optional, see
1114     https://nbformat.readthedocs.io/en/latest/format_description.html. So
1115     if a notebook has empty metadata, we will try to parse it anyway.
1116     """
1117     language = nb.get("metadata", {}).get("language_info", {}).get("name", None)
1118     if language is not None and language != "python":
1119         raise NothingChanged from None
1120
1121
1122 def format_ipynb_string(src_contents: str, *, fast: bool, mode: Mode) -> FileContent:
1123     """Format Jupyter notebook.
1124
1125     Operate cell-by-cell, only on code cells, only for Python notebooks.
1126     If the ``.ipynb`` originally had a trailing newline, it'll be preserved.
1127     """
1128     trailing_newline = src_contents[-1] == "\n"
1129     modified = False
1130     nb = json.loads(src_contents)
1131     validate_metadata(nb)
1132     for cell in nb["cells"]:
1133         if cell.get("cell_type", None) == "code":
1134             try:
1135                 src = "".join(cell["source"])
1136                 dst = format_cell(src, fast=fast, mode=mode)
1137             except NothingChanged:
1138                 pass
1139             else:
1140                 cell["source"] = dst.splitlines(keepends=True)
1141                 modified = True
1142     if modified:
1143         dst_contents = json.dumps(nb, indent=1, ensure_ascii=False)
1144         if trailing_newline:
1145             dst_contents = dst_contents + "\n"
1146         return dst_contents
1147     else:
1148         raise NothingChanged
1149
1150
1151 def format_str(src_contents: str, *, mode: Mode) -> str:
1152     """Reformat a string and return new contents.
1153
1154     `mode` determines formatting options, such as how many characters per line are
1155     allowed.  Example:
1156
1157     >>> import black
1158     >>> print(black.format_str("def f(arg:str='')->None:...", mode=black.Mode()))
1159     def f(arg: str = "") -> None:
1160         ...
1161
1162     A more complex example:
1163
1164     >>> print(
1165     ...   black.format_str(
1166     ...     "def f(arg:str='')->None: hey",
1167     ...     mode=black.Mode(
1168     ...       target_versions={black.TargetVersion.PY36},
1169     ...       line_length=10,
1170     ...       string_normalization=False,
1171     ...       is_pyi=False,
1172     ...     ),
1173     ...   ),
1174     ... )
1175     def f(
1176         arg: str = '',
1177     ) -> None:
1178         hey
1179
1180     """
1181     dst_contents = _format_str_once(src_contents, mode=mode)
1182     # Forced second pass to work around optional trailing commas (becoming
1183     # forced trailing commas on pass 2) interacting differently with optional
1184     # parentheses.  Admittedly ugly.
1185     if src_contents != dst_contents:
1186         return _format_str_once(dst_contents, mode=mode)
1187     return dst_contents
1188
1189
1190 def _format_str_once(src_contents: str, *, mode: Mode) -> str:
1191     src_node = lib2to3_parse(src_contents.lstrip(), mode.target_versions)
1192     dst_contents = []
1193     if mode.target_versions:
1194         versions = mode.target_versions
1195     else:
1196         future_imports = get_future_imports(src_node)
1197         versions = detect_target_versions(src_node, future_imports=future_imports)
1198
1199     normalize_fmt_off(src_node, preview=mode.preview)
1200     lines = LineGenerator(mode=mode)
1201     elt = EmptyLineTracker(is_pyi=mode.is_pyi)
1202     empty_line = Line(mode=mode)
1203     after = 0
1204     split_line_features = {
1205         feature
1206         for feature in {Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF}
1207         if supports_feature(versions, feature)
1208     }
1209     for current_line in lines.visit(src_node):
1210         dst_contents.append(str(empty_line) * after)
1211         before, after = elt.maybe_empty_lines(current_line)
1212         dst_contents.append(str(empty_line) * before)
1213         for line in transform_line(
1214             current_line, mode=mode, features=split_line_features
1215         ):
1216             dst_contents.append(str(line))
1217     return "".join(dst_contents)
1218
1219
1220 def decode_bytes(src: bytes) -> Tuple[FileContent, Encoding, NewLine]:
1221     """Return a tuple of (decoded_contents, encoding, newline).
1222
1223     `newline` is either CRLF or LF but `decoded_contents` is decoded with
1224     universal newlines (i.e. only contains LF).
1225     """
1226     srcbuf = io.BytesIO(src)
1227     encoding, lines = tokenize.detect_encoding(srcbuf.readline)
1228     if not lines:
1229         return "", encoding, "\n"
1230
1231     newline = "\r\n" if b"\r\n" == lines[0][-2:] else "\n"
1232     srcbuf.seek(0)
1233     with io.TextIOWrapper(srcbuf, encoding) as tiow:
1234         return tiow.read(), encoding, newline
1235
1236
1237 def get_features_used(  # noqa: C901
1238     node: Node, *, future_imports: Optional[Set[str]] = None
1239 ) -> Set[Feature]:
1240     """Return a set of (relatively) new Python features used in this file.
1241
1242     Currently looking for:
1243     - f-strings;
1244     - self-documenting expressions in f-strings (f"{x=}");
1245     - underscores in numeric literals;
1246     - trailing commas after * or ** in function signatures and calls;
1247     - positional only arguments in function signatures and lambdas;
1248     - assignment expression;
1249     - relaxed decorator syntax;
1250     - usage of __future__ flags (annotations);
1251     - print / exec statements;
1252     """
1253     features: Set[Feature] = set()
1254     if future_imports:
1255         features |= {
1256             FUTURE_FLAG_TO_FEATURE[future_import]
1257             for future_import in future_imports
1258             if future_import in FUTURE_FLAG_TO_FEATURE
1259         }
1260
1261     for n in node.pre_order():
1262         if is_string_token(n):
1263             value_head = n.value[:2]
1264             if value_head in {'f"', 'F"', "f'", "F'", "rf", "fr", "RF", "FR"}:
1265                 features.add(Feature.F_STRINGS)
1266                 if Feature.DEBUG_F_STRINGS not in features:
1267                     for span_beg, span_end in iter_fexpr_spans(n.value):
1268                         if n.value[span_beg : span_end - 1].rstrip().endswith("="):
1269                             features.add(Feature.DEBUG_F_STRINGS)
1270                             break
1271
1272         elif is_number_token(n):
1273             if "_" in n.value:
1274                 features.add(Feature.NUMERIC_UNDERSCORES)
1275
1276         elif n.type == token.SLASH:
1277             if n.parent and n.parent.type in {
1278                 syms.typedargslist,
1279                 syms.arglist,
1280                 syms.varargslist,
1281             }:
1282                 features.add(Feature.POS_ONLY_ARGUMENTS)
1283
1284         elif n.type == token.COLONEQUAL:
1285             features.add(Feature.ASSIGNMENT_EXPRESSIONS)
1286
1287         elif n.type == syms.decorator:
1288             if len(n.children) > 1 and not is_simple_decorator_expression(
1289                 n.children[1]
1290             ):
1291                 features.add(Feature.RELAXED_DECORATORS)
1292
1293         elif (
1294             n.type in {syms.typedargslist, syms.arglist}
1295             and n.children
1296             and n.children[-1].type == token.COMMA
1297         ):
1298             if n.type == syms.typedargslist:
1299                 feature = Feature.TRAILING_COMMA_IN_DEF
1300             else:
1301                 feature = Feature.TRAILING_COMMA_IN_CALL
1302
1303             for ch in n.children:
1304                 if ch.type in STARS:
1305                     features.add(feature)
1306
1307                 if ch.type == syms.argument:
1308                     for argch in ch.children:
1309                         if argch.type in STARS:
1310                             features.add(feature)
1311
1312         elif (
1313             n.type in {syms.return_stmt, syms.yield_expr}
1314             and len(n.children) >= 2
1315             and n.children[1].type == syms.testlist_star_expr
1316             and any(child.type == syms.star_expr for child in n.children[1].children)
1317         ):
1318             features.add(Feature.UNPACKING_ON_FLOW)
1319
1320         elif (
1321             n.type == syms.annassign
1322             and len(n.children) >= 4
1323             and n.children[3].type == syms.testlist_star_expr
1324         ):
1325             features.add(Feature.ANN_ASSIGN_EXTENDED_RHS)
1326
1327         elif (
1328             n.type == syms.except_clause
1329             and len(n.children) >= 2
1330             and n.children[1].type == token.STAR
1331         ):
1332             features.add(Feature.EXCEPT_STAR)
1333
1334         elif n.type in {syms.subscriptlist, syms.trailer} and any(
1335             child.type == syms.star_expr for child in n.children
1336         ):
1337             features.add(Feature.VARIADIC_GENERICS)
1338
1339         elif (
1340             n.type == syms.tname_star
1341             and len(n.children) == 3
1342             and n.children[2].type == syms.star_expr
1343         ):
1344             features.add(Feature.VARIADIC_GENERICS)
1345
1346     return features
1347
1348
1349 def detect_target_versions(
1350     node: Node, *, future_imports: Optional[Set[str]] = None
1351 ) -> Set[TargetVersion]:
1352     """Detect the version to target based on the nodes used."""
1353     features = get_features_used(node, future_imports=future_imports)
1354     return {
1355         version for version in TargetVersion if features <= VERSION_TO_FEATURES[version]
1356     }
1357
1358
1359 def get_future_imports(node: Node) -> Set[str]:
1360     """Return a set of __future__ imports in the file."""
1361     imports: Set[str] = set()
1362
1363     def get_imports_from_children(children: List[LN]) -> Generator[str, None, None]:
1364         for child in children:
1365             if isinstance(child, Leaf):
1366                 if child.type == token.NAME:
1367                     yield child.value
1368
1369             elif child.type == syms.import_as_name:
1370                 orig_name = child.children[0]
1371                 assert isinstance(orig_name, Leaf), "Invalid syntax parsing imports"
1372                 assert orig_name.type == token.NAME, "Invalid syntax parsing imports"
1373                 yield orig_name.value
1374
1375             elif child.type == syms.import_as_names:
1376                 yield from get_imports_from_children(child.children)
1377
1378             else:
1379                 raise AssertionError("Invalid syntax parsing imports")
1380
1381     for child in node.children:
1382         if child.type != syms.simple_stmt:
1383             break
1384
1385         first_child = child.children[0]
1386         if isinstance(first_child, Leaf):
1387             # Continue looking if we see a docstring; otherwise stop.
1388             if (
1389                 len(child.children) == 2
1390                 and first_child.type == token.STRING
1391                 and child.children[1].type == token.NEWLINE
1392             ):
1393                 continue
1394
1395             break
1396
1397         elif first_child.type == syms.import_from:
1398             module_name = first_child.children[1]
1399             if not isinstance(module_name, Leaf) or module_name.value != "__future__":
1400                 break
1401
1402             imports |= set(get_imports_from_children(first_child.children[3:]))
1403         else:
1404             break
1405
1406     return imports
1407
1408
1409 def assert_equivalent(src: str, dst: str) -> None:
1410     """Raise AssertionError if `src` and `dst` aren't equivalent."""
1411     try:
1412         src_ast = parse_ast(src)
1413     except Exception as exc:
1414         raise AssertionError(
1415             "cannot use --safe with this file; failed to parse source file AST: "
1416             f"{exc}\n"
1417             "This could be caused by running Black with an older Python version "
1418             "that does not support new syntax used in your source file."
1419         ) from exc
1420
1421     try:
1422         dst_ast = parse_ast(dst)
1423     except Exception as exc:
1424         log = dump_to_file("".join(traceback.format_tb(exc.__traceback__)), dst)
1425         raise AssertionError(
1426             f"INTERNAL ERROR: Black produced invalid code: {exc}. "
1427             "Please report a bug on https://github.com/psf/black/issues.  "
1428             f"This invalid output might be helpful: {log}"
1429         ) from None
1430
1431     src_ast_str = "\n".join(stringify_ast(src_ast))
1432     dst_ast_str = "\n".join(stringify_ast(dst_ast))
1433     if src_ast_str != dst_ast_str:
1434         log = dump_to_file(diff(src_ast_str, dst_ast_str, "src", "dst"))
1435         raise AssertionError(
1436             "INTERNAL ERROR: Black produced code that is not equivalent to the"
1437             " source.  Please report a bug on "
1438             f"https://github.com/psf/black/issues.  This diff might be helpful: {log}"
1439         ) from None
1440
1441
1442 def assert_stable(src: str, dst: str, mode: Mode) -> None:
1443     """Raise AssertionError if `dst` reformats differently the second time."""
1444     # We shouldn't call format_str() here, because that formats the string
1445     # twice and may hide a bug where we bounce back and forth between two
1446     # versions.
1447     newdst = _format_str_once(dst, mode=mode)
1448     if dst != newdst:
1449         log = dump_to_file(
1450             str(mode),
1451             diff(src, dst, "source", "first pass"),
1452             diff(dst, newdst, "first pass", "second pass"),
1453         )
1454         raise AssertionError(
1455             "INTERNAL ERROR: Black produced different code on the second pass of the"
1456             " formatter.  Please report a bug on https://github.com/psf/black/issues."
1457             f"  This diff might be helpful: {log}"
1458         ) from None
1459
1460
1461 @contextmanager
1462 def nullcontext() -> Iterator[None]:
1463     """Return an empty context manager.
1464
1465     To be used like `nullcontext` in Python 3.7.
1466     """
1467     yield
1468
1469
1470 def patch_click() -> None:
1471     """Make Click not crash on Python 3.6 with LANG=C.
1472
1473     On certain misconfigured environments, Python 3 selects the ASCII encoding as the
1474     default which restricts paths that it can access during the lifetime of the
1475     application.  Click refuses to work in this scenario by raising a RuntimeError.
1476
1477     In case of Black the likelihood that non-ASCII characters are going to be used in
1478     file paths is minimal since it's Python source code.  Moreover, this crash was
1479     spurious on Python 3.7 thanks to PEP 538 and PEP 540.
1480     """
1481     modules: List[Any] = []
1482     try:
1483         from click import core
1484     except ImportError:
1485         pass
1486     else:
1487         modules.append(core)
1488     try:
1489         # Removed in Click 8.1.0 and newer; we keep this around for users who have
1490         # older versions installed.
1491         from click import _unicodefun  # type: ignore
1492     except ImportError:
1493         pass
1494     else:
1495         modules.append(_unicodefun)
1496
1497     for module in modules:
1498         if hasattr(module, "_verify_python3_env"):
1499             module._verify_python3_env = lambda: None  # type: ignore
1500         if hasattr(module, "_verify_python_env"):
1501             module._verify_python_env = lambda: None  # type: ignore
1502
1503
1504 def patched_main() -> None:
1505     maybe_install_uvloop()
1506     freeze_support()
1507     patch_click()
1508     main()
1509
1510
1511 if __name__ == "__main__":
1512     patched_main()