]> 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:

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