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.
2 from concurrent.futures import Executor, ThreadPoolExecutor, ProcessPoolExecutor
3 from contextlib import contextmanager
4 from datetime import datetime
7 from multiprocessing import Manager, freeze_support
9 from pathlib import Path
29 from dataclasses import replace
32 from black.const import DEFAULT_LINE_LENGTH, DEFAULT_INCLUDES, DEFAULT_EXCLUDES
33 from black.const import STDIN_PLACEHOLDER
34 from black.nodes import STARS, syms, is_simple_decorator_expression
35 from black.lines import Line, EmptyLineTracker
36 from black.linegen import transform_line, LineGenerator, LN
37 from black.comments import normalize_fmt_off
38 from black.mode import Mode, TargetVersion
39 from black.mode import Feature, supports_feature, VERSION_TO_FEATURES
40 from black.cache import read_cache, write_cache, get_cache_info, filter_cached, Cache
41 from black.concurrency import cancel, shutdown
42 from black.output import dump_to_file, diff, color_diff, out, err
43 from black.report import Report, Changed
44 from black.files import find_project_root, find_pyproject_toml, parse_pyproject_toml
45 from black.files import gen_python_files, get_gitignore, normalize_path_maybe_ignore
46 from black.files import wrap_stream_for_windows
47 from black.parsing import InvalidInput # noqa F401
48 from black.parsing import lib2to3_parse, parse_ast, stringify_ast
52 from blib2to3.pytree import Node, Leaf
53 from blib2to3.pgen2 import token
55 from _black_version import version as __version__
64 class NothingChanged(UserWarning):
65 """Raised when reformatted code is the same as source."""
68 class WriteBack(Enum):
76 def from_configuration(
77 cls, *, check: bool, diff: bool, color: bool = False
79 if check and not diff:
85 return cls.DIFF if diff else cls.YES
88 # Legacy name, left for integrations.
92 def read_pyproject_toml(
93 ctx: click.Context, param: click.Parameter, value: Optional[str]
95 """Inject Black configuration from "pyproject.toml" into defaults in `ctx`.
97 Returns the path to a successfully found and read configuration file, None
101 value = find_pyproject_toml(ctx.params.get("src", ()))
106 config = parse_pyproject_toml(value)
107 except (OSError, ValueError) as e:
108 raise click.FileError(
109 filename=value, hint=f"Error reading configuration file: {e}"
115 # Sanitize the values to be Click friendly. For more information please see:
116 # https://github.com/psf/black/issues/1458
117 # https://github.com/pallets/click/issues/1567
119 k: str(v) if not isinstance(v, (list, dict)) else v
120 for k, v in config.items()
123 target_version = config.get("target_version")
124 if target_version is not None and not isinstance(target_version, list):
125 raise click.BadOptionUsage(
126 "target-version", "Config key target-version must be a list"
129 default_map: Dict[str, Any] = {}
131 default_map.update(ctx.default_map)
132 default_map.update(config)
134 ctx.default_map = default_map
138 def target_version_option_callback(
139 c: click.Context, p: Union[click.Option, click.Parameter], v: Tuple[str, ...]
140 ) -> List[TargetVersion]:
141 """Compute the target versions from a --target-version flag.
143 This is its own function because mypy couldn't infer the type correctly
144 when it was a lambda, causing mypyc trouble.
146 return [TargetVersion[val.upper()] for val in v]
149 def re_compile_maybe_verbose(regex: str) -> Pattern[str]:
150 """Compile a regular expression string in `regex`.
152 If it contains newlines, use verbose mode.
155 regex = "(?x)" + regex
156 compiled: Pattern[str] = re.compile(regex)
162 param: click.Parameter,
163 value: Optional[str],
164 ) -> Optional[Pattern]:
166 return re_compile_maybe_verbose(value) if value is not None else None
168 raise click.BadParameter("Not a valid regular expression")
171 @click.command(context_settings=dict(help_option_names=["-h", "--help"]))
172 @click.option("-c", "--code", type=str, help="Format the code passed in as a string.")
177 default=DEFAULT_LINE_LENGTH,
178 help="How many characters per line to allow.",
184 type=click.Choice([v.name.lower() for v in TargetVersion]),
185 callback=target_version_option_callback,
188 "Python versions that should be supported by Black's output. [default: per-file"
196 "Format all input files like typing stubs regardless of file extension (useful"
197 " when piping source on standard input)."
202 "--skip-string-normalization",
204 help="Don't normalize string quotes or prefixes.",
208 "--skip-magic-trailing-comma",
210 help="Don't use trailing commas as a reason to split lines.",
213 "--experimental-string-processing",
217 "Experimental option that performs more normalization on string literals."
218 " Currently disabled because it leads to some crashes."
225 "Don't write the files back, just return the status. Return code 0 means"
226 " nothing would change. Return code 1 means some files would be reformatted."
227 " Return code 123 means there was an internal error."
233 help="Don't write the files back, just output a diff for each file on stdout.",
236 "--color/--no-color",
238 help="Show colored diff. Only applies when `--diff` is given.",
243 help="If --fast given, skip temporary sanity checks. [default: --safe]",
248 default=DEFAULT_INCLUDES,
249 callback=validate_regex,
251 "A regular expression that matches files and directories that should be"
252 " included on recursive searches. An empty value means all files are included"
253 " regardless of the name. Use forward slashes for directories on all platforms"
254 " (Windows, too). Exclusions are calculated first, inclusions later."
261 callback=validate_regex,
263 "A regular expression that matches files and directories that should be"
264 " excluded on recursive searches. An empty value means no paths are excluded."
265 " Use forward slashes for directories on all platforms (Windows, too)."
266 " Exclusions are calculated first, inclusions later. [default:"
267 f" {DEFAULT_EXCLUDES}]"
274 callback=validate_regex,
276 "Like --exclude, but adds additional files and directories on top of the"
277 " excluded ones. (Useful if you simply want to add to the default)"
283 callback=validate_regex,
285 "Like --exclude, but files and directories matching this regex will be "
286 "excluded even when they are passed explicitly as arguments."
293 "The name of the file when passing it through stdin. Useful to make "
294 "sure Black will respect --force-exclude option on some "
295 "editors that rely on using stdin."
303 "Don't emit non-error messages to stderr. Errors are still emitted; silence"
304 " those with 2>/dev/null."
312 "Also emit messages to stderr about files that were not changed or were ignored"
313 " due to exclusion patterns."
316 @click.version_option(version=__version__)
321 exists=True, file_okay=True, dir_okay=True, readable=True, allow_dash=True
336 callback=read_pyproject_toml,
337 help="Read configuration from FILE path.",
344 target_version: List[TargetVersion],
350 skip_string_normalization: bool,
351 skip_magic_trailing_comma: bool,
352 experimental_string_processing: bool,
356 exclude: Optional[Pattern],
357 extend_exclude: Optional[Pattern],
358 force_exclude: Optional[Pattern],
359 stdin_filename: Optional[str],
360 src: Tuple[str, ...],
361 config: Optional[str],
363 """The uncompromising code formatter."""
364 write_back = WriteBack.from_configuration(check=check, diff=diff, color=color)
366 versions = set(target_version)
368 # We'll autodetect later.
371 target_versions=versions,
372 line_length=line_length,
374 string_normalization=not skip_string_normalization,
375 magic_trailing_comma=not skip_magic_trailing_comma,
376 experimental_string_processing=experimental_string_processing,
378 if config and verbose:
379 out(f"Using configuration from {config}.", bold=False, fg="blue")
381 print(format_str(code, mode=mode))
383 report = Report(check=check, diff=diff, quiet=quiet, verbose=verbose)
384 sources = get_sources(
391 extend_exclude=extend_exclude,
392 force_exclude=force_exclude,
394 stdin_filename=stdin_filename,
399 "No Python files are present to be formatted. Nothing to do 😴",
405 if len(sources) == 1:
409 write_back=write_back,
415 sources=sources, fast=fast, write_back=write_back, mode=mode, report=report
418 if verbose or not quiet:
419 out("Oh no! 💥 💔 💥" if report.return_code else "All done! ✨ 🍰 ✨")
420 click.secho(str(report), err=True)
421 ctx.exit(report.return_code)
427 src: Tuple[str, ...],
430 include: Pattern[str],
431 exclude: Optional[Pattern[str]],
432 extend_exclude: Optional[Pattern[str]],
433 force_exclude: Optional[Pattern[str]],
435 stdin_filename: Optional[str],
437 """Compute the set of files to be formatted."""
439 root = find_project_root(src)
440 sources: Set[Path] = set()
441 path_empty(src, "No Path provided. Nothing to do 😴", quiet, verbose, ctx)
444 exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES)
445 gitignore = get_gitignore(root)
450 if s == "-" and stdin_filename:
451 p = Path(stdin_filename)
457 if is_stdin or p.is_file():
458 normalized_path = normalize_path_maybe_ignore(p, root, report)
459 if normalized_path is None:
462 normalized_path = "/" + normalized_path
463 # Hard-exclude any files that matches the `--force-exclude` regex.
465 force_exclude_match = force_exclude.search(normalized_path)
467 force_exclude_match = None
468 if force_exclude_match and force_exclude_match.group(0):
469 report.path_ignored(p, "matches the --force-exclude regular expression")
473 p = Path(f"{STDIN_PLACEHOLDER}{str(p)}")
492 err(f"invalid path: {s}")
497 src: Sized, msg: str, quiet: bool, verbose: bool, ctx: click.Context
500 Exit if there is no `src` provided for formatting
502 if not src and (verbose or not quiet):
508 src: Path, fast: bool, write_back: WriteBack, mode: Mode, report: "Report"
510 """Reformat a single file under `src` without spawning child processes.
512 `fast`, `write_back`, and `mode` options are passed to
513 :func:`format_file_in_place` or :func:`format_stdin_to_stdout`.
520 elif str(src).startswith(STDIN_PLACEHOLDER):
522 # Use the original name again in case we want to print something
524 src = Path(str(src)[len(STDIN_PLACEHOLDER) :])
529 if src.suffix == ".pyi":
530 mode = replace(mode, is_pyi=True)
531 if format_stdin_to_stdout(fast=fast, write_back=write_back, mode=mode):
532 changed = Changed.YES
535 if write_back not in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
536 cache = read_cache(mode)
537 res_src = src.resolve()
538 res_src_s = str(res_src)
539 if res_src_s in cache and cache[res_src_s] == get_cache_info(res_src):
540 changed = Changed.CACHED
541 if changed is not Changed.CACHED and format_file_in_place(
542 src, fast=fast, write_back=write_back, mode=mode
544 changed = Changed.YES
545 if (write_back is WriteBack.YES and changed is not Changed.CACHED) or (
546 write_back is WriteBack.CHECK and changed is Changed.NO
548 write_cache(cache, [src], mode)
549 report.done(src, changed)
550 except Exception as exc:
552 traceback.print_exc()
553 report.failed(src, str(exc))
557 sources: Set[Path], fast: bool, write_back: WriteBack, mode: Mode, report: "Report"
559 """Reformat multiple files using a ProcessPoolExecutor."""
561 loop = asyncio.get_event_loop()
562 worker_count = os.cpu_count()
563 if sys.platform == "win32":
564 # Work around https://bugs.python.org/issue26903
565 worker_count = min(worker_count, 60)
567 executor = ProcessPoolExecutor(max_workers=worker_count)
568 except (ImportError, OSError):
569 # we arrive here if the underlying system does not support multi-processing
570 # like in AWS Lambda or Termux, in which case we gracefully fallback to
571 # a ThreadPoolExecutor with just a single worker (more workers would not do us
572 # any good due to the Global Interpreter Lock)
573 executor = ThreadPoolExecutor(max_workers=1)
576 loop.run_until_complete(
580 write_back=write_back,
589 if executor is not None:
593 async def schedule_formatting(
596 write_back: WriteBack,
599 loop: asyncio.AbstractEventLoop,
602 """Run formatting of `sources` in parallel using the provided `executor`.
604 (Use ProcessPoolExecutors for actual parallelism.)
606 `write_back`, `fast`, and `mode` options are passed to
607 :func:`format_file_in_place`.
610 if write_back not in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
611 cache = read_cache(mode)
612 sources, cached = filter_cached(cache, sources)
613 for src in sorted(cached):
614 report.done(src, Changed.CACHED)
619 sources_to_cache = []
621 if write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
622 # For diff output, we need locks to ensure we don't interleave output
623 # from different processes.
625 lock = manager.Lock()
627 asyncio.ensure_future(
628 loop.run_in_executor(
629 executor, format_file_in_place, src, fast, mode, write_back, lock
632 for src in sorted(sources)
634 pending = tasks.keys()
636 loop.add_signal_handler(signal.SIGINT, cancel, pending)
637 loop.add_signal_handler(signal.SIGTERM, cancel, pending)
638 except NotImplementedError:
639 # There are no good alternatives for these on Windows.
642 done, _ = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)
644 src = tasks.pop(task)
646 cancelled.append(task)
647 elif task.exception():
648 report.failed(src, str(task.exception()))
650 changed = Changed.YES if task.result() else Changed.NO
651 # If the file was written back or was successfully checked as
652 # well-formatted, store this information in the cache.
653 if write_back is WriteBack.YES or (
654 write_back is WriteBack.CHECK and changed is Changed.NO
656 sources_to_cache.append(src)
657 report.done(src, changed)
659 await asyncio.gather(*cancelled, loop=loop, return_exceptions=True)
661 write_cache(cache, sources_to_cache, mode)
664 def format_file_in_place(
668 write_back: WriteBack = WriteBack.NO,
669 lock: Any = None, # multiprocessing.Manager().Lock() is some crazy proxy
671 """Format file under `src` path. Return True if changed.
673 If `write_back` is DIFF, write a diff to stdout. If it is YES, write reformatted
675 `mode` and `fast` options are passed to :func:`format_file_contents`.
677 if src.suffix == ".pyi":
678 mode = replace(mode, is_pyi=True)
680 then = datetime.utcfromtimestamp(src.stat().st_mtime)
681 with open(src, "rb") as buf:
682 src_contents, encoding, newline = decode_bytes(buf.read())
684 dst_contents = format_file_contents(src_contents, fast=fast, mode=mode)
685 except NothingChanged:
688 if write_back == WriteBack.YES:
689 with open(src, "w", encoding=encoding, newline=newline) as f:
690 f.write(dst_contents)
691 elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
692 now = datetime.utcnow()
693 src_name = f"{src}\t{then} +0000"
694 dst_name = f"{src}\t{now} +0000"
695 diff_contents = diff(src_contents, dst_contents, src_name, dst_name)
697 if write_back == WriteBack.COLOR_DIFF:
698 diff_contents = color_diff(diff_contents)
700 with lock or nullcontext():
701 f = io.TextIOWrapper(
707 f = wrap_stream_for_windows(f)
708 f.write(diff_contents)
714 def format_stdin_to_stdout(
715 fast: bool, *, write_back: WriteBack = WriteBack.NO, mode: Mode
717 """Format file on stdin. Return True if changed.
719 If `write_back` is YES, write reformatted code back to stdout. If it is DIFF,
720 write a diff to stdout. The `mode` argument is passed to
721 :func:`format_file_contents`.
723 then = datetime.utcnow()
724 src, encoding, newline = decode_bytes(sys.stdin.buffer.read())
727 dst = format_file_contents(src, fast=fast, mode=mode)
730 except NothingChanged:
734 f = io.TextIOWrapper(
735 sys.stdout.buffer, encoding=encoding, newline=newline, write_through=True
737 if write_back == WriteBack.YES:
739 elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
740 now = datetime.utcnow()
741 src_name = f"STDIN\t{then} +0000"
742 dst_name = f"STDOUT\t{now} +0000"
743 d = diff(src, dst, src_name, dst_name)
744 if write_back == WriteBack.COLOR_DIFF:
746 f = wrap_stream_for_windows(f)
751 def format_file_contents(src_contents: str, *, fast: bool, mode: Mode) -> FileContent:
752 """Reformat contents of a file and return new contents.
754 If `fast` is False, additionally confirm that the reformatted code is
755 valid by calling :func:`assert_equivalent` and :func:`assert_stable` on it.
756 `mode` is passed to :func:`format_str`.
758 if not src_contents.strip():
761 dst_contents = format_str(src_contents, mode=mode)
762 if src_contents == dst_contents:
766 assert_equivalent(src_contents, dst_contents)
768 # Forced second pass to work around optional trailing commas (becoming
769 # forced trailing commas on pass 2) interacting differently with optional
770 # parentheses. Admittedly ugly.
771 dst_contents_pass2 = format_str(dst_contents, mode=mode)
772 if dst_contents != dst_contents_pass2:
773 dst_contents = dst_contents_pass2
774 assert_equivalent(src_contents, dst_contents, pass_num=2)
775 assert_stable(src_contents, dst_contents, mode=mode)
776 # Note: no need to explicitly call `assert_stable` if `dst_contents` was
777 # the same as `dst_contents_pass2`.
781 def format_str(src_contents: str, *, mode: Mode) -> FileContent:
782 """Reformat a string and return new contents.
784 `mode` determines formatting options, such as how many characters per line are
788 >>> print(black.format_str("def f(arg:str='')->None:...", mode=black.Mode()))
789 def f(arg: str = "") -> None:
792 A more complex example:
795 ... black.format_str(
796 ... "def f(arg:str='')->None: hey",
798 ... target_versions={black.TargetVersion.PY36},
800 ... string_normalization=False,
811 src_node = lib2to3_parse(src_contents.lstrip(), mode.target_versions)
813 future_imports = get_future_imports(src_node)
814 if mode.target_versions:
815 versions = mode.target_versions
817 versions = detect_target_versions(src_node)
818 normalize_fmt_off(src_node)
819 lines = LineGenerator(
821 remove_u_prefix="unicode_literals" in future_imports
822 or supports_feature(versions, Feature.UNICODE_LITERALS),
824 elt = EmptyLineTracker(is_pyi=mode.is_pyi)
825 empty_line = Line(mode=mode)
827 split_line_features = {
829 for feature in {Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF}
830 if supports_feature(versions, feature)
832 for current_line in lines.visit(src_node):
833 dst_contents.append(str(empty_line) * after)
834 before, after = elt.maybe_empty_lines(current_line)
835 dst_contents.append(str(empty_line) * before)
836 for line in transform_line(
837 current_line, mode=mode, features=split_line_features
839 dst_contents.append(str(line))
840 return "".join(dst_contents)
843 def decode_bytes(src: bytes) -> Tuple[FileContent, Encoding, NewLine]:
844 """Return a tuple of (decoded_contents, encoding, newline).
846 `newline` is either CRLF or LF but `decoded_contents` is decoded with
847 universal newlines (i.e. only contains LF).
849 srcbuf = io.BytesIO(src)
850 encoding, lines = tokenize.detect_encoding(srcbuf.readline)
852 return "", encoding, "\n"
854 newline = "\r\n" if b"\r\n" == lines[0][-2:] else "\n"
856 with io.TextIOWrapper(srcbuf, encoding) as tiow:
857 return tiow.read(), encoding, newline
860 def get_features_used(node: Node) -> Set[Feature]:
861 """Return a set of (relatively) new Python features used in this file.
863 Currently looking for:
865 - underscores in numeric literals;
866 - trailing commas after * or ** in function signatures and calls;
867 - positional only arguments in function signatures and lambdas;
868 - assignment expression;
869 - relaxed decorator syntax;
871 features: Set[Feature] = set()
872 for n in node.pre_order():
873 if n.type == token.STRING:
874 value_head = n.value[:2] # type: ignore
875 if value_head in {'f"', 'F"', "f'", "F'", "rf", "fr", "RF", "FR"}:
876 features.add(Feature.F_STRINGS)
878 elif n.type == token.NUMBER:
879 if "_" in n.value: # type: ignore
880 features.add(Feature.NUMERIC_UNDERSCORES)
882 elif n.type == token.SLASH:
883 if n.parent and n.parent.type in {syms.typedargslist, syms.arglist}:
884 features.add(Feature.POS_ONLY_ARGUMENTS)
886 elif n.type == token.COLONEQUAL:
887 features.add(Feature.ASSIGNMENT_EXPRESSIONS)
889 elif n.type == syms.decorator:
890 if len(n.children) > 1 and not is_simple_decorator_expression(
893 features.add(Feature.RELAXED_DECORATORS)
896 n.type in {syms.typedargslist, syms.arglist}
898 and n.children[-1].type == token.COMMA
900 if n.type == syms.typedargslist:
901 feature = Feature.TRAILING_COMMA_IN_DEF
903 feature = Feature.TRAILING_COMMA_IN_CALL
905 for ch in n.children:
907 features.add(feature)
909 if ch.type == syms.argument:
910 for argch in ch.children:
911 if argch.type in STARS:
912 features.add(feature)
917 def detect_target_versions(node: Node) -> Set[TargetVersion]:
918 """Detect the version to target based on the nodes used."""
919 features = get_features_used(node)
921 version for version in TargetVersion if features <= VERSION_TO_FEATURES[version]
925 def get_future_imports(node: Node) -> Set[str]:
926 """Return a set of __future__ imports in the file."""
927 imports: Set[str] = set()
929 def get_imports_from_children(children: List[LN]) -> Generator[str, None, None]:
930 for child in children:
931 if isinstance(child, Leaf):
932 if child.type == token.NAME:
935 elif child.type == syms.import_as_name:
936 orig_name = child.children[0]
937 assert isinstance(orig_name, Leaf), "Invalid syntax parsing imports"
938 assert orig_name.type == token.NAME, "Invalid syntax parsing imports"
939 yield orig_name.value
941 elif child.type == syms.import_as_names:
942 yield from get_imports_from_children(child.children)
945 raise AssertionError("Invalid syntax parsing imports")
947 for child in node.children:
948 if child.type != syms.simple_stmt:
951 first_child = child.children[0]
952 if isinstance(first_child, Leaf):
953 # Continue looking if we see a docstring; otherwise stop.
955 len(child.children) == 2
956 and first_child.type == token.STRING
957 and child.children[1].type == token.NEWLINE
963 elif first_child.type == syms.import_from:
964 module_name = first_child.children[1]
965 if not isinstance(module_name, Leaf) or module_name.value != "__future__":
968 imports |= set(get_imports_from_children(first_child.children[3:]))
975 def assert_equivalent(src: str, dst: str, *, pass_num: int = 1) -> None:
976 """Raise AssertionError if `src` and `dst` aren't equivalent."""
978 src_ast = parse_ast(src)
979 except Exception as exc:
980 raise AssertionError(
981 "cannot use --safe with this file; failed to parse source file. AST"
982 f" error message: {exc}"
986 dst_ast = parse_ast(dst)
987 except Exception as exc:
988 log = dump_to_file("".join(traceback.format_tb(exc.__traceback__)), dst)
989 raise AssertionError(
990 f"INTERNAL ERROR: Black produced invalid code on pass {pass_num}: {exc}. "
991 "Please report a bug on https://github.com/psf/black/issues. "
992 f"This invalid output might be helpful: {log}"
995 src_ast_str = "\n".join(stringify_ast(src_ast))
996 dst_ast_str = "\n".join(stringify_ast(dst_ast))
997 if src_ast_str != dst_ast_str:
998 log = dump_to_file(diff(src_ast_str, dst_ast_str, "src", "dst"))
999 raise AssertionError(
1000 "INTERNAL ERROR: Black produced code that is not equivalent to the"
1001 f" source on pass {pass_num}. Please report a bug on "
1002 f"https://github.com/psf/black/issues. This diff might be helpful: {log}"
1006 def assert_stable(src: str, dst: str, mode: Mode) -> None:
1007 """Raise AssertionError if `dst` reformats differently the second time."""
1008 newdst = format_str(dst, mode=mode)
1012 diff(src, dst, "source", "first pass"),
1013 diff(dst, newdst, "first pass", "second pass"),
1015 raise AssertionError(
1016 "INTERNAL ERROR: Black produced different code on the second pass of the"
1017 " formatter. Please report a bug on https://github.com/psf/black/issues."
1018 f" This diff might be helpful: {log}"
1023 def nullcontext() -> Iterator[None]:
1024 """Return an empty context manager.
1026 To be used like `nullcontext` in Python 3.7.
1031 def patch_click() -> None:
1032 """Make Click not crash on Python 3.6 with LANG=C.
1034 On certain misconfigured environments, Python 3 selects the ASCII encoding as the
1035 default which restricts paths that it can access during the lifetime of the
1036 application. Click refuses to work in this scenario by raising a RuntimeError.
1038 In case of Black the likelihood that non-ASCII characters are going to be used in
1039 file paths is minimal since it's Python source code. Moreover, this crash was
1040 spurious on Python 3.7 thanks to PEP 538 and PEP 540.
1043 from click import core
1044 from click import _unicodefun # type: ignore
1045 except ModuleNotFoundError:
1048 for module in (core, _unicodefun):
1049 if hasattr(module, "_verify_python3_env"):
1050 module._verify_python3_env = lambda: None # type: ignore
1051 if hasattr(module, "_verify_python_env"):
1052 module._verify_python_env = lambda: None # type: ignore
1055 def patched_main() -> None:
1061 if __name__ == "__main__":