Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/black/__init__.py: 19%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import io
2import json
3import platform
4import re
5import sys
6import tokenize
7import traceback
8from collections.abc import (
9 Collection,
10 Generator,
11 Iterator,
12 MutableMapping,
13 Sequence,
14 Sized,
15)
16from contextlib import contextmanager
17from dataclasses import replace
18from datetime import datetime, timezone
19from enum import Enum
20from json.decoder import JSONDecodeError
21from pathlib import Path
22from re import Pattern
23from typing import Any, Optional, Union
25import click
26from click.core import ParameterSource
27from mypy_extensions import mypyc_attr
28from pathspec import PathSpec
29from pathspec.patterns.gitwildmatch import GitWildMatchPatternError
31from _black_version import version as __version__
32from black.cache import Cache
33from black.comments import normalize_fmt_off
34from black.const import (
35 DEFAULT_EXCLUDES,
36 DEFAULT_INCLUDES,
37 DEFAULT_LINE_LENGTH,
38 STDIN_PLACEHOLDER,
39)
40from black.files import (
41 best_effort_relative_path,
42 find_project_root,
43 find_pyproject_toml,
44 find_user_pyproject_toml,
45 gen_python_files,
46 get_gitignore,
47 parse_pyproject_toml,
48 path_is_excluded,
49 resolves_outside_root_or_cannot_stat,
50 wrap_stream_for_windows,
51)
52from black.handle_ipynb_magics import (
53 PYTHON_CELL_MAGICS,
54 jupyter_dependencies_are_installed,
55 mask_cell,
56 put_trailing_semicolon_back,
57 remove_trailing_semicolon,
58 unmask_cell,
59 validate_cell,
60)
61from black.linegen import LN, LineGenerator, transform_line
62from black.lines import EmptyLineTracker, LinesBlock
63from black.mode import FUTURE_FLAG_TO_FEATURE, VERSION_TO_FEATURES, Feature
64from black.mode import Mode as Mode # re-exported
65from black.mode import Preview, TargetVersion, supports_feature
66from black.nodes import STARS, is_number_token, is_simple_decorator_expression, syms
67from black.output import color_diff, diff, dump_to_file, err, ipynb_diff, out
68from black.parsing import ( # noqa F401
69 ASTSafetyError,
70 InvalidInput,
71 lib2to3_parse,
72 parse_ast,
73 stringify_ast,
74)
75from black.ranges import (
76 adjusted_lines,
77 convert_unchanged_lines,
78 parse_line_ranges,
79 sanitized_lines,
80)
81from black.report import Changed, NothingChanged, Report
82from blib2to3.pgen2 import token
83from blib2to3.pytree import Leaf, Node
85COMPILED = Path(__file__).suffix in (".pyd", ".so")
87# types
88FileContent = str
89Encoding = str
90NewLine = str
93class WriteBack(Enum):
94 NO = 0
95 YES = 1
96 DIFF = 2
97 CHECK = 3
98 COLOR_DIFF = 4
100 @classmethod
101 def from_configuration(
102 cls, *, check: bool, diff: bool, color: bool = False
103 ) -> "WriteBack":
104 if check and not diff:
105 return cls.CHECK
107 if diff and color:
108 return cls.COLOR_DIFF
110 return cls.DIFF if diff else cls.YES
113# Legacy name, left for integrations.
114FileMode = Mode
117def read_pyproject_toml(
118 ctx: click.Context, param: click.Parameter, value: Optional[str]
119) -> Optional[str]:
120 """Inject Black configuration from "pyproject.toml" into defaults in `ctx`.
122 Returns the path to a successfully found and read configuration file, None
123 otherwise.
124 """
125 if not value:
126 value = find_pyproject_toml(
127 ctx.params.get("src", ()), ctx.params.get("stdin_filename", None)
128 )
129 if value is None:
130 return None
132 try:
133 config = parse_pyproject_toml(value)
134 except (OSError, ValueError) as e:
135 raise click.FileError(
136 filename=value, hint=f"Error reading configuration file: {e}"
137 ) from None
139 if not config:
140 return None
141 else:
142 spellcheck_pyproject_toml_keys(ctx, list(config), value)
143 # Sanitize the values to be Click friendly. For more information please see:
144 # https://github.com/psf/black/issues/1458
145 # https://github.com/pallets/click/issues/1567
146 config = {
147 k: str(v) if not isinstance(v, (list, dict)) else v
148 for k, v in config.items()
149 }
151 target_version = config.get("target_version")
152 if target_version is not None and not isinstance(target_version, list):
153 raise click.BadOptionUsage(
154 "target-version", "Config key target-version must be a list"
155 )
157 exclude = config.get("exclude")
158 if exclude is not None and not isinstance(exclude, str):
159 raise click.BadOptionUsage("exclude", "Config key exclude must be a string")
161 extend_exclude = config.get("extend_exclude")
162 if extend_exclude is not None and not isinstance(extend_exclude, str):
163 raise click.BadOptionUsage(
164 "extend-exclude", "Config key extend-exclude must be a string"
165 )
167 line_ranges = config.get("line_ranges")
168 if line_ranges is not None:
169 raise click.BadOptionUsage(
170 "line-ranges", "Cannot use line-ranges in the pyproject.toml file."
171 )
173 default_map: dict[str, Any] = {}
174 if ctx.default_map:
175 default_map.update(ctx.default_map)
176 default_map.update(config)
178 ctx.default_map = default_map
179 return value
182def spellcheck_pyproject_toml_keys(
183 ctx: click.Context, config_keys: list[str], config_file_path: str
184) -> None:
185 invalid_keys: list[str] = []
186 available_config_options = {param.name for param in ctx.command.params}
187 for key in config_keys:
188 if key not in available_config_options:
189 invalid_keys.append(key)
190 if invalid_keys:
191 keys_str = ", ".join(map(repr, invalid_keys))
192 out(
193 f"Invalid config keys detected: {keys_str} (in {config_file_path})",
194 fg="red",
195 )
198def target_version_option_callback(
199 c: click.Context, p: Union[click.Option, click.Parameter], v: tuple[str, ...]
200) -> list[TargetVersion]:
201 """Compute the target versions from a --target-version flag.
203 This is its own function because mypy couldn't infer the type correctly
204 when it was a lambda, causing mypyc trouble.
205 """
206 return [TargetVersion[val.upper()] for val in v]
209def enable_unstable_feature_callback(
210 c: click.Context, p: Union[click.Option, click.Parameter], v: tuple[str, ...]
211) -> list[Preview]:
212 """Compute the features from an --enable-unstable-feature flag."""
213 return [Preview[val] for val in v]
216def re_compile_maybe_verbose(regex: str) -> Pattern[str]:
217 """Compile a regular expression string in `regex`.
219 If it contains newlines, use verbose mode.
220 """
221 if "\n" in regex:
222 regex = "(?x)" + regex
223 compiled: Pattern[str] = re.compile(regex)
224 return compiled
227def validate_regex(
228 ctx: click.Context,
229 param: click.Parameter,
230 value: Optional[str],
231) -> Optional[Pattern[str]]:
232 try:
233 return re_compile_maybe_verbose(value) if value is not None else None
234 except re.error as e:
235 raise click.BadParameter(f"Not a valid regular expression: {e}") from None
238@click.command(
239 context_settings={"help_option_names": ["-h", "--help"]},
240 # While Click does set this field automatically using the docstring, mypyc
241 # (annoyingly) strips 'em so we need to set it here too.
242 help="The uncompromising code formatter.",
243)
244@click.option("-c", "--code", type=str, help="Format the code passed in as a string.")
245@click.option(
246 "-l",
247 "--line-length",
248 type=int,
249 default=DEFAULT_LINE_LENGTH,
250 help="How many characters per line to allow.",
251 show_default=True,
252)
253@click.option(
254 "-t",
255 "--target-version",
256 type=click.Choice([v.name.lower() for v in TargetVersion]),
257 callback=target_version_option_callback,
258 multiple=True,
259 help=(
260 "Python versions that should be supported by Black's output. You should"
261 " include all versions that your code supports. By default, Black will infer"
262 " target versions from the project metadata in pyproject.toml. If this does"
263 " not yield conclusive results, Black will use per-file auto-detection."
264 ),
265)
266@click.option(
267 "--pyi",
268 is_flag=True,
269 help=(
270 "Format all input files like typing stubs regardless of file extension. This"
271 " is useful when piping source on standard input."
272 ),
273)
274@click.option(
275 "--ipynb",
276 is_flag=True,
277 help=(
278 "Format all input files like Jupyter Notebooks regardless of file extension."
279 " This is useful when piping source on standard input."
280 ),
281)
282@click.option(
283 "--python-cell-magics",
284 multiple=True,
285 help=(
286 "When processing Jupyter Notebooks, add the given magic to the list"
287 f" of known python-magics ({', '.join(sorted(PYTHON_CELL_MAGICS))})."
288 " Useful for formatting cells with custom python magics."
289 ),
290 default=[],
291)
292@click.option(
293 "-x",
294 "--skip-source-first-line",
295 is_flag=True,
296 help="Skip the first line of the source code.",
297)
298@click.option(
299 "-S",
300 "--skip-string-normalization",
301 is_flag=True,
302 help="Don't normalize string quotes or prefixes.",
303)
304@click.option(
305 "-C",
306 "--skip-magic-trailing-comma",
307 is_flag=True,
308 help="Don't use trailing commas as a reason to split lines.",
309)
310@click.option(
311 "--preview",
312 is_flag=True,
313 help=(
314 "Enable potentially disruptive style changes that may be added to Black's main"
315 " functionality in the next major release."
316 ),
317)
318@click.option(
319 "--unstable",
320 is_flag=True,
321 help=(
322 "Enable potentially disruptive style changes that have known bugs or are not"
323 " currently expected to make it into the stable style Black's next major"
324 " release. Implies --preview."
325 ),
326)
327@click.option(
328 "--enable-unstable-feature",
329 type=click.Choice([v.name for v in Preview]),
330 callback=enable_unstable_feature_callback,
331 multiple=True,
332 help=(
333 "Enable specific features included in the `--unstable` style. Requires"
334 " `--preview`. No compatibility guarantees are provided on the behavior"
335 " or existence of any unstable features."
336 ),
337)
338@click.option(
339 "--check",
340 is_flag=True,
341 help=(
342 "Don't write the files back, just return the status. Return code 0 means"
343 " nothing would change. Return code 1 means some files would be reformatted."
344 " Return code 123 means there was an internal error."
345 ),
346)
347@click.option(
348 "--diff",
349 is_flag=True,
350 help=(
351 "Don't write the files back, just output a diff to indicate what changes"
352 " Black would've made. They are printed to stdout so capturing them is simple."
353 ),
354)
355@click.option(
356 "--color/--no-color",
357 is_flag=True,
358 help="Show (or do not show) colored diff. Only applies when --diff is given.",
359)
360@click.option(
361 "--line-ranges",
362 multiple=True,
363 metavar="START-END",
364 help=(
365 "When specified, Black will try its best to only format these lines. This"
366 " option can be specified multiple times, and a union of the lines will be"
367 " formatted. Each range must be specified as two integers connected by a `-`:"
368 " `<START>-<END>`. The `<START>` and `<END>` integer indices are 1-based and"
369 " inclusive on both ends."
370 ),
371 default=(),
372)
373@click.option(
374 "--fast/--safe",
375 is_flag=True,
376 help=(
377 "By default, Black performs an AST safety check after formatting your code."
378 " The --fast flag turns off this check and the --safe flag explicitly enables"
379 " it. [default: --safe]"
380 ),
381)
382@click.option(
383 "--required-version",
384 type=str,
385 help=(
386 "Require a specific version of Black to be running. This is useful for"
387 " ensuring that all contributors to your project are using the same"
388 " version, because different versions of Black may format code a little"
389 " differently. This option can be set in a configuration file for consistent"
390 " results across environments."
391 ),
392)
393@click.option(
394 "--exclude",
395 type=str,
396 callback=validate_regex,
397 help=(
398 "A regular expression that matches files and directories that should be"
399 " excluded on recursive searches. An empty value means no paths are excluded."
400 " Use forward slashes for directories on all platforms (Windows, too)."
401 " By default, Black also ignores all paths listed in .gitignore. Changing this"
402 f" value will override all default exclusions. [default: {DEFAULT_EXCLUDES}]"
403 ),
404 show_default=False,
405)
406@click.option(
407 "--extend-exclude",
408 type=str,
409 callback=validate_regex,
410 help=(
411 "Like --exclude, but adds additional files and directories on top of the"
412 " default values instead of overriding them."
413 ),
414)
415@click.option(
416 "--force-exclude",
417 type=str,
418 callback=validate_regex,
419 help=(
420 "Like --exclude, but files and directories matching this regex will be excluded"
421 " even when they are passed explicitly as arguments. This is useful when"
422 " invoking Black programmatically on changed files, such as in a pre-commit"
423 " hook or editor plugin."
424 ),
425)
426@click.option(
427 "--stdin-filename",
428 type=str,
429 is_eager=True,
430 help=(
431 "The name of the file when passing it through stdin. Useful to make sure Black"
432 " will respect the --force-exclude option on some editors that rely on using"
433 " stdin."
434 ),
435)
436@click.option(
437 "--include",
438 type=str,
439 default=DEFAULT_INCLUDES,
440 callback=validate_regex,
441 help=(
442 "A regular expression that matches files and directories that should be"
443 " included on recursive searches. An empty value means all files are included"
444 " regardless of the name. Use forward slashes for directories on all platforms"
445 " (Windows, too). Overrides all exclusions, including from .gitignore and"
446 " command line options."
447 ),
448 show_default=True,
449)
450@click.option(
451 "-W",
452 "--workers",
453 type=click.IntRange(min=1),
454 default=None,
455 help=(
456 "When Black formats multiple files, it may use a process pool to speed up"
457 " formatting. This option controls the number of parallel workers. This can"
458 " also be specified via the BLACK_NUM_WORKERS environment variable. Defaults"
459 " to the number of CPUs in the system."
460 ),
461)
462@click.option(
463 "-q",
464 "--quiet",
465 is_flag=True,
466 help=(
467 "Stop emitting all non-critical output. Error messages will still be emitted"
468 " (which can silenced by 2>/dev/null)."
469 ),
470)
471@click.option(
472 "-v",
473 "--verbose",
474 is_flag=True,
475 help=(
476 "Emit messages about files that were not changed or were ignored due to"
477 " exclusion patterns. If Black is using a configuration file, a message"
478 " detailing which one it is using will be emitted."
479 ),
480)
481@click.version_option(
482 version=__version__,
483 message=(
484 f"%(prog)s, %(version)s (compiled: {'yes' if COMPILED else 'no'})\n"
485 f"Python ({platform.python_implementation()}) {platform.python_version()}"
486 ),
487)
488@click.argument(
489 "src",
490 nargs=-1,
491 type=click.Path(
492 exists=True, file_okay=True, dir_okay=True, readable=True, allow_dash=True
493 ),
494 is_eager=True,
495 metavar="SRC ...",
496)
497@click.option(
498 "--config",
499 type=click.Path(
500 exists=True,
501 file_okay=True,
502 dir_okay=False,
503 readable=True,
504 allow_dash=False,
505 path_type=str,
506 ),
507 is_eager=True,
508 callback=read_pyproject_toml,
509 help="Read configuration options from a configuration file.",
510)
511@click.pass_context
512def main( # noqa: C901
513 ctx: click.Context,
514 code: Optional[str],
515 line_length: int,
516 target_version: list[TargetVersion],
517 check: bool,
518 diff: bool,
519 line_ranges: Sequence[str],
520 color: bool,
521 fast: bool,
522 pyi: bool,
523 ipynb: bool,
524 python_cell_magics: Sequence[str],
525 skip_source_first_line: bool,
526 skip_string_normalization: bool,
527 skip_magic_trailing_comma: bool,
528 preview: bool,
529 unstable: bool,
530 enable_unstable_feature: list[Preview],
531 quiet: bool,
532 verbose: bool,
533 required_version: Optional[str],
534 include: Pattern[str],
535 exclude: Optional[Pattern[str]],
536 extend_exclude: Optional[Pattern[str]],
537 force_exclude: Optional[Pattern[str]],
538 stdin_filename: Optional[str],
539 workers: Optional[int],
540 src: tuple[str, ...],
541 config: Optional[str],
542) -> None:
543 """The uncompromising code formatter."""
544 ctx.ensure_object(dict)
546 assert sys.version_info >= (3, 9), "Black requires Python 3.9+"
547 if sys.version_info[:3] == (3, 12, 5):
548 out(
549 "Python 3.12.5 has a memory safety issue that can cause Black's "
550 "AST safety checks to fail. "
551 "Please upgrade to Python 3.12.6 or downgrade to Python 3.12.4"
552 )
553 ctx.exit(1)
555 if src and code is not None:
556 out(
557 main.get_usage(ctx)
558 + "\n\n'SRC' and 'code' cannot be passed simultaneously."
559 )
560 ctx.exit(1)
561 if not src and code is None:
562 out(main.get_usage(ctx) + "\n\nOne of 'SRC' or 'code' is required.")
563 ctx.exit(1)
565 # It doesn't do anything if --unstable is also passed, so just allow it.
566 if enable_unstable_feature and not (preview or unstable):
567 out(
568 main.get_usage(ctx)
569 + "\n\n'--enable-unstable-feature' requires '--preview'."
570 )
571 ctx.exit(1)
573 root, method = (
574 find_project_root(src, stdin_filename) if code is None else (None, None)
575 )
576 ctx.obj["root"] = root
578 if verbose:
579 if root:
580 out(
581 f"Identified `{root}` as project root containing a {method}.",
582 fg="blue",
583 )
585 if config:
586 config_source = ctx.get_parameter_source("config")
587 user_level_config = str(find_user_pyproject_toml())
588 if config == user_level_config:
589 out(
590 "Using configuration from user-level config at "
591 f"'{user_level_config}'.",
592 fg="blue",
593 )
594 elif config_source in (
595 ParameterSource.DEFAULT,
596 ParameterSource.DEFAULT_MAP,
597 ):
598 out("Using configuration from project root.", fg="blue")
599 else:
600 out(f"Using configuration in '{config}'.", fg="blue")
601 if ctx.default_map:
602 for param, value in ctx.default_map.items():
603 out(f"{param}: {value}")
605 error_msg = "Oh no! 💥 💔 💥"
606 if (
607 required_version
608 and required_version != __version__
609 and required_version != __version__.split(".")[0]
610 ):
611 err(
612 f"{error_msg} The required version `{required_version}` does not match"
613 f" the running version `{__version__}`!"
614 )
615 ctx.exit(1)
616 if ipynb and pyi:
617 err("Cannot pass both `pyi` and `ipynb` flags!")
618 ctx.exit(1)
620 write_back = WriteBack.from_configuration(check=check, diff=diff, color=color)
621 if target_version:
622 versions = set(target_version)
623 else:
624 # We'll autodetect later.
625 versions = set()
626 mode = Mode(
627 target_versions=versions,
628 line_length=line_length,
629 is_pyi=pyi,
630 is_ipynb=ipynb,
631 skip_source_first_line=skip_source_first_line,
632 string_normalization=not skip_string_normalization,
633 magic_trailing_comma=not skip_magic_trailing_comma,
634 preview=preview,
635 unstable=unstable,
636 python_cell_magics=set(python_cell_magics),
637 enabled_features=set(enable_unstable_feature),
638 )
640 lines: list[tuple[int, int]] = []
641 if line_ranges:
642 if ipynb:
643 err("Cannot use --line-ranges with ipynb files.")
644 ctx.exit(1)
646 try:
647 lines = parse_line_ranges(line_ranges)
648 except ValueError as e:
649 err(str(e))
650 ctx.exit(1)
652 if code is not None:
653 # Run in quiet mode by default with -c; the extra output isn't useful.
654 # You can still pass -v to get verbose output.
655 quiet = True
657 report = Report(check=check, diff=diff, quiet=quiet, verbose=verbose)
659 if code is not None:
660 reformat_code(
661 content=code,
662 fast=fast,
663 write_back=write_back,
664 mode=mode,
665 report=report,
666 lines=lines,
667 )
668 else:
669 assert root is not None # root is only None if code is not None
670 try:
671 sources = get_sources(
672 root=root,
673 src=src,
674 quiet=quiet,
675 verbose=verbose,
676 include=include,
677 exclude=exclude,
678 extend_exclude=extend_exclude,
679 force_exclude=force_exclude,
680 report=report,
681 stdin_filename=stdin_filename,
682 )
683 except GitWildMatchPatternError:
684 ctx.exit(1)
686 path_empty(
687 sources,
688 "No Python files are present to be formatted. Nothing to do 😴",
689 quiet,
690 verbose,
691 ctx,
692 )
694 if len(sources) == 1:
695 reformat_one(
696 src=sources.pop(),
697 fast=fast,
698 write_back=write_back,
699 mode=mode,
700 report=report,
701 lines=lines,
702 )
703 else:
704 from black.concurrency import reformat_many
706 if lines:
707 err("Cannot use --line-ranges to format multiple files.")
708 ctx.exit(1)
709 reformat_many(
710 sources=sources,
711 fast=fast,
712 write_back=write_back,
713 mode=mode,
714 report=report,
715 workers=workers,
716 )
718 if verbose or not quiet:
719 if code is None and (verbose or report.change_count or report.failure_count):
720 out()
721 out(error_msg if report.return_code else "All done! ✨ 🍰 ✨")
722 if code is None:
723 click.echo(str(report), err=True)
724 ctx.exit(report.return_code)
727def get_sources(
728 *,
729 root: Path,
730 src: tuple[str, ...],
731 quiet: bool,
732 verbose: bool,
733 include: Pattern[str],
734 exclude: Optional[Pattern[str]],
735 extend_exclude: Optional[Pattern[str]],
736 force_exclude: Optional[Pattern[str]],
737 report: "Report",
738 stdin_filename: Optional[str],
739) -> set[Path]:
740 """Compute the set of files to be formatted."""
741 sources: set[Path] = set()
743 assert root.is_absolute(), f"INTERNAL ERROR: `root` must be absolute but is {root}"
744 using_default_exclude = exclude is None
745 exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES) if exclude is None else exclude
746 gitignore: Optional[dict[Path, PathSpec]] = None
747 root_gitignore = get_gitignore(root)
749 for s in src:
750 if s == "-" and stdin_filename:
751 path = Path(stdin_filename)
752 if path_is_excluded(stdin_filename, force_exclude):
753 report.path_ignored(
754 path,
755 "--stdin-filename matches the --force-exclude regular expression",
756 )
757 continue
758 is_stdin = True
759 else:
760 path = Path(s)
761 is_stdin = False
763 # Compare the logic here to the logic in `gen_python_files`.
764 if is_stdin or path.is_file():
765 if resolves_outside_root_or_cannot_stat(path, root, report):
766 if verbose:
767 out(f'Skipping invalid source: "{path}"', fg="red")
768 continue
770 root_relative_path = best_effort_relative_path(path, root).as_posix()
771 root_relative_path = "/" + root_relative_path
773 # Hard-exclude any files that matches the `--force-exclude` regex.
774 if path_is_excluded(root_relative_path, force_exclude):
775 report.path_ignored(
776 path, "matches the --force-exclude regular expression"
777 )
778 continue
780 if is_stdin:
781 path = Path(f"{STDIN_PLACEHOLDER}{str(path)}")
783 if path.suffix == ".ipynb" and not jupyter_dependencies_are_installed(
784 warn=verbose or not quiet
785 ):
786 continue
788 if verbose:
789 out(f'Found input source: "{path}"', fg="blue")
790 sources.add(path)
791 elif path.is_dir():
792 path = root / (path.resolve().relative_to(root))
793 if verbose:
794 out(f'Found input source directory: "{path}"', fg="blue")
796 if using_default_exclude:
797 gitignore = {
798 root: root_gitignore,
799 path: get_gitignore(path),
800 }
801 sources.update(
802 gen_python_files(
803 path.iterdir(),
804 root,
805 include,
806 exclude,
807 extend_exclude,
808 force_exclude,
809 report,
810 gitignore,
811 verbose=verbose,
812 quiet=quiet,
813 )
814 )
815 elif s == "-":
816 if verbose:
817 out("Found input source stdin", fg="blue")
818 sources.add(path)
819 else:
820 err(f"invalid path: {s}")
822 return sources
825def path_empty(
826 src: Sized, msg: str, quiet: bool, verbose: bool, ctx: click.Context
827) -> None:
828 """
829 Exit if there is no `src` provided for formatting
830 """
831 if not src:
832 if verbose or not quiet:
833 out(msg)
834 ctx.exit(0)
837def reformat_code(
838 content: str,
839 fast: bool,
840 write_back: WriteBack,
841 mode: Mode,
842 report: Report,
843 *,
844 lines: Collection[tuple[int, int]] = (),
845) -> None:
846 """
847 Reformat and print out `content` without spawning child processes.
848 Similar to `reformat_one`, but for string content.
850 `fast`, `write_back`, and `mode` options are passed to
851 :func:`format_file_in_place` or :func:`format_stdin_to_stdout`.
852 """
853 path = Path("<string>")
854 try:
855 changed = Changed.NO
856 if format_stdin_to_stdout(
857 content=content, fast=fast, write_back=write_back, mode=mode, lines=lines
858 ):
859 changed = Changed.YES
860 report.done(path, changed)
861 except Exception as exc:
862 if report.verbose:
863 traceback.print_exc()
864 report.failed(path, str(exc))
867# diff-shades depends on being to monkeypatch this function to operate. I know it's
868# not ideal, but this shouldn't cause any issues ... hopefully. ~ichard26
869@mypyc_attr(patchable=True)
870def reformat_one(
871 src: Path,
872 fast: bool,
873 write_back: WriteBack,
874 mode: Mode,
875 report: "Report",
876 *,
877 lines: Collection[tuple[int, int]] = (),
878) -> None:
879 """Reformat a single file under `src` without spawning child processes.
881 `fast`, `write_back`, and `mode` options are passed to
882 :func:`format_file_in_place` or :func:`format_stdin_to_stdout`.
883 """
884 try:
885 changed = Changed.NO
887 if str(src) == "-":
888 is_stdin = True
889 elif str(src).startswith(STDIN_PLACEHOLDER):
890 is_stdin = True
891 # Use the original name again in case we want to print something
892 # to the user
893 src = Path(str(src)[len(STDIN_PLACEHOLDER) :])
894 else:
895 is_stdin = False
897 if is_stdin:
898 if src.suffix == ".pyi":
899 mode = replace(mode, is_pyi=True)
900 elif src.suffix == ".ipynb":
901 mode = replace(mode, is_ipynb=True)
902 if format_stdin_to_stdout(
903 fast=fast, write_back=write_back, mode=mode, lines=lines
904 ):
905 changed = Changed.YES
906 else:
907 cache = Cache.read(mode)
908 if write_back not in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
909 if not cache.is_changed(src):
910 changed = Changed.CACHED
911 if changed is not Changed.CACHED and format_file_in_place(
912 src, fast=fast, write_back=write_back, mode=mode, lines=lines
913 ):
914 changed = Changed.YES
915 if (write_back is WriteBack.YES and changed is not Changed.CACHED) or (
916 write_back is WriteBack.CHECK and changed is Changed.NO
917 ):
918 cache.write([src])
919 report.done(src, changed)
920 except Exception as exc:
921 if report.verbose:
922 traceback.print_exc()
923 report.failed(src, str(exc))
926def format_file_in_place(
927 src: Path,
928 fast: bool,
929 mode: Mode,
930 write_back: WriteBack = WriteBack.NO,
931 lock: Any = None, # multiprocessing.Manager().Lock() is some crazy proxy
932 *,
933 lines: Collection[tuple[int, int]] = (),
934) -> bool:
935 """Format file under `src` path. Return True if changed.
937 If `write_back` is DIFF, write a diff to stdout. If it is YES, write reformatted
938 code to the file.
939 `mode` and `fast` options are passed to :func:`format_file_contents`.
940 """
941 if src.suffix == ".pyi":
942 mode = replace(mode, is_pyi=True)
943 elif src.suffix == ".ipynb":
944 mode = replace(mode, is_ipynb=True)
946 then = datetime.fromtimestamp(src.stat().st_mtime, timezone.utc)
947 header = b""
948 with open(src, "rb") as buf:
949 if mode.skip_source_first_line:
950 header = buf.readline()
951 src_contents, encoding, newline = decode_bytes(buf.read())
952 try:
953 dst_contents = format_file_contents(
954 src_contents, fast=fast, mode=mode, lines=lines
955 )
956 except NothingChanged:
957 return False
958 except JSONDecodeError:
959 raise ValueError(
960 f"File '{src}' cannot be parsed as valid Jupyter notebook."
961 ) from None
962 src_contents = header.decode(encoding) + src_contents
963 dst_contents = header.decode(encoding) + dst_contents
965 if write_back == WriteBack.YES:
966 with open(src, "w", encoding=encoding, newline=newline) as f:
967 f.write(dst_contents)
968 elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
969 now = datetime.now(timezone.utc)
970 src_name = f"{src}\t{then}"
971 dst_name = f"{src}\t{now}"
972 if mode.is_ipynb:
973 diff_contents = ipynb_diff(src_contents, dst_contents, src_name, dst_name)
974 else:
975 diff_contents = diff(src_contents, dst_contents, src_name, dst_name)
977 if write_back == WriteBack.COLOR_DIFF:
978 diff_contents = color_diff(diff_contents)
980 with lock or nullcontext():
981 f = io.TextIOWrapper(
982 sys.stdout.buffer,
983 encoding=encoding,
984 newline=newline,
985 write_through=True,
986 )
987 f = wrap_stream_for_windows(f)
988 f.write(diff_contents)
989 f.detach()
991 return True
994def format_stdin_to_stdout(
995 fast: bool,
996 *,
997 content: Optional[str] = None,
998 write_back: WriteBack = WriteBack.NO,
999 mode: Mode,
1000 lines: Collection[tuple[int, int]] = (),
1001) -> bool:
1002 """Format file on stdin. Return True if changed.
1004 If content is None, it's read from sys.stdin.
1006 If `write_back` is YES, write reformatted code back to stdout. If it is DIFF,
1007 write a diff to stdout. The `mode` argument is passed to
1008 :func:`format_file_contents`.
1009 """
1010 then = datetime.now(timezone.utc)
1012 if content is None:
1013 src, encoding, newline = decode_bytes(sys.stdin.buffer.read())
1014 else:
1015 src, encoding, newline = content, "utf-8", ""
1017 dst = src
1018 try:
1019 dst = format_file_contents(src, fast=fast, mode=mode, lines=lines)
1020 return True
1022 except NothingChanged:
1023 return False
1025 finally:
1026 f = io.TextIOWrapper(
1027 sys.stdout.buffer, encoding=encoding, newline=newline, write_through=True
1028 )
1029 if write_back == WriteBack.YES:
1030 # Make sure there's a newline after the content
1031 if dst and dst[-1] != "\n":
1032 dst += "\n"
1033 f.write(dst)
1034 elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
1035 now = datetime.now(timezone.utc)
1036 src_name = f"STDIN\t{then}"
1037 dst_name = f"STDOUT\t{now}"
1038 d = diff(src, dst, src_name, dst_name)
1039 if write_back == WriteBack.COLOR_DIFF:
1040 d = color_diff(d)
1041 f = wrap_stream_for_windows(f)
1042 f.write(d)
1043 f.detach()
1046def check_stability_and_equivalence(
1047 src_contents: str,
1048 dst_contents: str,
1049 *,
1050 mode: Mode,
1051 lines: Collection[tuple[int, int]] = (),
1052) -> None:
1053 """Perform stability and equivalence checks.
1055 Raise AssertionError if source and destination contents are not
1056 equivalent, or if a second pass of the formatter would format the
1057 content differently.
1058 """
1059 assert_equivalent(src_contents, dst_contents)
1060 assert_stable(src_contents, dst_contents, mode=mode, lines=lines)
1063def format_file_contents(
1064 src_contents: str,
1065 *,
1066 fast: bool,
1067 mode: Mode,
1068 lines: Collection[tuple[int, int]] = (),
1069) -> FileContent:
1070 """Reformat contents of a file and return new contents.
1072 If `fast` is False, additionally confirm that the reformatted code is
1073 valid by calling :func:`assert_equivalent` and :func:`assert_stable` on it.
1074 `mode` is passed to :func:`format_str`.
1075 """
1076 if mode.is_ipynb:
1077 dst_contents = format_ipynb_string(src_contents, fast=fast, mode=mode)
1078 else:
1079 dst_contents = format_str(src_contents, mode=mode, lines=lines)
1080 if src_contents == dst_contents:
1081 raise NothingChanged
1083 if not fast and not mode.is_ipynb:
1084 # Jupyter notebooks will already have been checked above.
1085 check_stability_and_equivalence(
1086 src_contents, dst_contents, mode=mode, lines=lines
1087 )
1088 return dst_contents
1091def format_cell(src: str, *, fast: bool, mode: Mode) -> str:
1092 """Format code in given cell of Jupyter notebook.
1094 General idea is:
1096 - if cell has trailing semicolon, remove it;
1097 - if cell has IPython magics, mask them;
1098 - format cell;
1099 - reinstate IPython magics;
1100 - reinstate trailing semicolon (if originally present);
1101 - strip trailing newlines.
1103 Cells with syntax errors will not be processed, as they
1104 could potentially be automagics or multi-line magics, which
1105 are currently not supported.
1106 """
1107 validate_cell(src, mode)
1108 src_without_trailing_semicolon, has_trailing_semicolon = remove_trailing_semicolon(
1109 src
1110 )
1111 try:
1112 masked_src, replacements = mask_cell(src_without_trailing_semicolon)
1113 except SyntaxError:
1114 raise NothingChanged from None
1115 masked_dst = format_str(masked_src, mode=mode)
1116 if not fast:
1117 check_stability_and_equivalence(masked_src, masked_dst, mode=mode)
1118 dst_without_trailing_semicolon = unmask_cell(masked_dst, replacements)
1119 dst = put_trailing_semicolon_back(
1120 dst_without_trailing_semicolon, has_trailing_semicolon
1121 )
1122 dst = dst.rstrip("\n")
1123 if dst == src:
1124 raise NothingChanged from None
1125 return dst
1128def validate_metadata(nb: MutableMapping[str, Any]) -> None:
1129 """If notebook is marked as non-Python, don't format it.
1131 All notebook metadata fields are optional, see
1132 https://nbformat.readthedocs.io/en/latest/format_description.html. So
1133 if a notebook has empty metadata, we will try to parse it anyway.
1134 """
1135 language = nb.get("metadata", {}).get("language_info", {}).get("name", None)
1136 if language is not None and language != "python":
1137 raise NothingChanged from None
1140def format_ipynb_string(src_contents: str, *, fast: bool, mode: Mode) -> FileContent:
1141 """Format Jupyter notebook.
1143 Operate cell-by-cell, only on code cells, only for Python notebooks.
1144 If the ``.ipynb`` originally had a trailing newline, it'll be preserved.
1145 """
1146 if not src_contents:
1147 raise NothingChanged
1149 trailing_newline = src_contents[-1] == "\n"
1150 modified = False
1151 nb = json.loads(src_contents)
1152 validate_metadata(nb)
1153 for cell in nb["cells"]:
1154 if cell.get("cell_type", None) == "code":
1155 try:
1156 src = "".join(cell["source"])
1157 dst = format_cell(src, fast=fast, mode=mode)
1158 except NothingChanged:
1159 pass
1160 else:
1161 cell["source"] = dst.splitlines(keepends=True)
1162 modified = True
1163 if modified:
1164 dst_contents = json.dumps(nb, indent=1, ensure_ascii=False)
1165 if trailing_newline:
1166 dst_contents = dst_contents + "\n"
1167 return dst_contents
1168 else:
1169 raise NothingChanged
1172def format_str(
1173 src_contents: str, *, mode: Mode, lines: Collection[tuple[int, int]] = ()
1174) -> str:
1175 """Reformat a string and return new contents.
1177 `mode` determines formatting options, such as how many characters per line are
1178 allowed. Example:
1180 >>> import black
1181 >>> print(black.format_str("def f(arg:str='')->None:...", mode=black.Mode()))
1182 def f(arg: str = "") -> None:
1183 ...
1185 A more complex example:
1187 >>> print(
1188 ... black.format_str(
1189 ... "def f(arg:str='')->None: hey",
1190 ... mode=black.Mode(
1191 ... target_versions={black.TargetVersion.PY36},
1192 ... line_length=10,
1193 ... string_normalization=False,
1194 ... is_pyi=False,
1195 ... ),
1196 ... ),
1197 ... )
1198 def f(
1199 arg: str = '',
1200 ) -> None:
1201 hey
1203 """
1204 if lines:
1205 lines = sanitized_lines(lines, src_contents)
1206 if not lines:
1207 return src_contents # Nothing to format
1208 dst_contents = _format_str_once(src_contents, mode=mode, lines=lines)
1209 # Forced second pass to work around optional trailing commas (becoming
1210 # forced trailing commas on pass 2) interacting differently with optional
1211 # parentheses. Admittedly ugly.
1212 if src_contents != dst_contents:
1213 if lines:
1214 lines = adjusted_lines(lines, src_contents, dst_contents)
1215 return _format_str_once(dst_contents, mode=mode, lines=lines)
1216 return dst_contents
1219def _format_str_once(
1220 src_contents: str, *, mode: Mode, lines: Collection[tuple[int, int]] = ()
1221) -> str:
1222 src_node = lib2to3_parse(src_contents.lstrip(), mode.target_versions)
1223 dst_blocks: list[LinesBlock] = []
1224 if mode.target_versions:
1225 versions = mode.target_versions
1226 else:
1227 future_imports = get_future_imports(src_node)
1228 versions = detect_target_versions(src_node, future_imports=future_imports)
1230 context_manager_features = {
1231 feature
1232 for feature in {Feature.PARENTHESIZED_CONTEXT_MANAGERS}
1233 if supports_feature(versions, feature)
1234 }
1235 normalize_fmt_off(src_node, mode, lines)
1236 if lines:
1237 # This should be called after normalize_fmt_off.
1238 convert_unchanged_lines(src_node, lines)
1240 line_generator = LineGenerator(mode=mode, features=context_manager_features)
1241 elt = EmptyLineTracker(mode=mode)
1242 split_line_features = {
1243 feature
1244 for feature in {
1245 Feature.TRAILING_COMMA_IN_CALL,
1246 Feature.TRAILING_COMMA_IN_DEF,
1247 }
1248 if supports_feature(versions, feature)
1249 }
1250 block: Optional[LinesBlock] = None
1251 for current_line in line_generator.visit(src_node):
1252 block = elt.maybe_empty_lines(current_line)
1253 dst_blocks.append(block)
1254 for line in transform_line(
1255 current_line, mode=mode, features=split_line_features
1256 ):
1257 block.content_lines.append(str(line))
1258 if dst_blocks:
1259 dst_blocks[-1].after = 0
1260 dst_contents = []
1261 for block in dst_blocks:
1262 dst_contents.extend(block.all_lines())
1263 if not dst_contents:
1264 # Use decode_bytes to retrieve the correct source newline (CRLF or LF),
1265 # and check if normalized_content has more than one line
1266 normalized_content, _, newline = decode_bytes(src_contents.encode("utf-8"))
1267 if "\n" in normalized_content:
1268 return newline
1269 return ""
1270 return "".join(dst_contents)
1273def decode_bytes(src: bytes) -> tuple[FileContent, Encoding, NewLine]:
1274 """Return a tuple of (decoded_contents, encoding, newline).
1276 `newline` is either CRLF or LF but `decoded_contents` is decoded with
1277 universal newlines (i.e. only contains LF).
1278 """
1279 srcbuf = io.BytesIO(src)
1280 encoding, lines = tokenize.detect_encoding(srcbuf.readline)
1281 if not lines:
1282 return "", encoding, "\n"
1284 newline = "\r\n" if b"\r\n" == lines[0][-2:] else "\n"
1285 srcbuf.seek(0)
1286 with io.TextIOWrapper(srcbuf, encoding) as tiow:
1287 return tiow.read(), encoding, newline
1290def get_features_used( # noqa: C901
1291 node: Node, *, future_imports: Optional[set[str]] = None
1292) -> set[Feature]:
1293 """Return a set of (relatively) new Python features used in this file.
1295 Currently looking for:
1296 - f-strings;
1297 - self-documenting expressions in f-strings (f"{x=}");
1298 - underscores in numeric literals;
1299 - trailing commas after * or ** in function signatures and calls;
1300 - positional only arguments in function signatures and lambdas;
1301 - assignment expression;
1302 - relaxed decorator syntax;
1303 - usage of __future__ flags (annotations);
1304 - print / exec statements;
1305 - parenthesized context managers;
1306 - match statements;
1307 - except* clause;
1308 - variadic generics;
1309 """
1310 features: set[Feature] = set()
1311 if future_imports:
1312 features |= {
1313 FUTURE_FLAG_TO_FEATURE[future_import]
1314 for future_import in future_imports
1315 if future_import in FUTURE_FLAG_TO_FEATURE
1316 }
1318 for n in node.pre_order():
1319 if n.type == token.FSTRING_START:
1320 features.add(Feature.F_STRINGS)
1321 elif (
1322 n.type == token.RBRACE
1323 and n.parent is not None
1324 and any(child.type == token.EQUAL for child in n.parent.children)
1325 ):
1326 features.add(Feature.DEBUG_F_STRINGS)
1328 elif is_number_token(n):
1329 if "_" in n.value:
1330 features.add(Feature.NUMERIC_UNDERSCORES)
1332 elif n.type == token.SLASH:
1333 if n.parent and n.parent.type in {
1334 syms.typedargslist,
1335 syms.arglist,
1336 syms.varargslist,
1337 }:
1338 features.add(Feature.POS_ONLY_ARGUMENTS)
1340 elif n.type == token.COLONEQUAL:
1341 features.add(Feature.ASSIGNMENT_EXPRESSIONS)
1343 elif n.type == syms.decorator:
1344 if len(n.children) > 1 and not is_simple_decorator_expression(
1345 n.children[1]
1346 ):
1347 features.add(Feature.RELAXED_DECORATORS)
1349 elif (
1350 n.type in {syms.typedargslist, syms.arglist}
1351 and n.children
1352 and n.children[-1].type == token.COMMA
1353 ):
1354 if n.type == syms.typedargslist:
1355 feature = Feature.TRAILING_COMMA_IN_DEF
1356 else:
1357 feature = Feature.TRAILING_COMMA_IN_CALL
1359 for ch in n.children:
1360 if ch.type in STARS:
1361 features.add(feature)
1363 if ch.type == syms.argument:
1364 for argch in ch.children:
1365 if argch.type in STARS:
1366 features.add(feature)
1368 elif (
1369 n.type in {syms.return_stmt, syms.yield_expr}
1370 and len(n.children) >= 2
1371 and n.children[1].type == syms.testlist_star_expr
1372 and any(child.type == syms.star_expr for child in n.children[1].children)
1373 ):
1374 features.add(Feature.UNPACKING_ON_FLOW)
1376 elif (
1377 n.type == syms.annassign
1378 and len(n.children) >= 4
1379 and n.children[3].type == syms.testlist_star_expr
1380 ):
1381 features.add(Feature.ANN_ASSIGN_EXTENDED_RHS)
1383 elif (
1384 n.type == syms.with_stmt
1385 and len(n.children) > 2
1386 and n.children[1].type == syms.atom
1387 ):
1388 atom_children = n.children[1].children
1389 if (
1390 len(atom_children) == 3
1391 and atom_children[0].type == token.LPAR
1392 and _contains_asexpr(atom_children[1])
1393 and atom_children[2].type == token.RPAR
1394 ):
1395 features.add(Feature.PARENTHESIZED_CONTEXT_MANAGERS)
1397 elif n.type == syms.match_stmt:
1398 features.add(Feature.PATTERN_MATCHING)
1400 elif (
1401 n.type == syms.except_clause
1402 and len(n.children) >= 2
1403 and n.children[1].type == token.STAR
1404 ):
1405 features.add(Feature.EXCEPT_STAR)
1407 elif n.type in {syms.subscriptlist, syms.trailer} and any(
1408 child.type == syms.star_expr for child in n.children
1409 ):
1410 features.add(Feature.VARIADIC_GENERICS)
1412 elif (
1413 n.type == syms.tname_star
1414 and len(n.children) == 3
1415 and n.children[2].type == syms.star_expr
1416 ):
1417 features.add(Feature.VARIADIC_GENERICS)
1419 elif n.type in (syms.type_stmt, syms.typeparams):
1420 features.add(Feature.TYPE_PARAMS)
1422 elif (
1423 n.type in (syms.typevartuple, syms.paramspec, syms.typevar)
1424 and n.children[-2].type == token.EQUAL
1425 ):
1426 features.add(Feature.TYPE_PARAM_DEFAULTS)
1428 return features
1431def _contains_asexpr(node: Union[Node, Leaf]) -> bool:
1432 """Return True if `node` contains an as-pattern."""
1433 if node.type == syms.asexpr_test:
1434 return True
1435 elif node.type == syms.atom:
1436 if (
1437 len(node.children) == 3
1438 and node.children[0].type == token.LPAR
1439 and node.children[2].type == token.RPAR
1440 ):
1441 return _contains_asexpr(node.children[1])
1442 elif node.type == syms.testlist_gexp:
1443 return any(_contains_asexpr(child) for child in node.children)
1444 return False
1447def detect_target_versions(
1448 node: Node, *, future_imports: Optional[set[str]] = None
1449) -> set[TargetVersion]:
1450 """Detect the version to target based on the nodes used."""
1451 features = get_features_used(node, future_imports=future_imports)
1452 return {
1453 version for version in TargetVersion if features <= VERSION_TO_FEATURES[version]
1454 }
1457def get_future_imports(node: Node) -> set[str]:
1458 """Return a set of __future__ imports in the file."""
1459 imports: set[str] = set()
1461 def get_imports_from_children(children: list[LN]) -> Generator[str, None, None]:
1462 for child in children:
1463 if isinstance(child, Leaf):
1464 if child.type == token.NAME:
1465 yield child.value
1467 elif child.type == syms.import_as_name:
1468 orig_name = child.children[0]
1469 assert isinstance(orig_name, Leaf), "Invalid syntax parsing imports"
1470 assert orig_name.type == token.NAME, "Invalid syntax parsing imports"
1471 yield orig_name.value
1473 elif child.type == syms.import_as_names:
1474 yield from get_imports_from_children(child.children)
1476 else:
1477 raise AssertionError("Invalid syntax parsing imports")
1479 for child in node.children:
1480 if child.type != syms.simple_stmt:
1481 break
1483 first_child = child.children[0]
1484 if isinstance(first_child, Leaf):
1485 # Continue looking if we see a docstring; otherwise stop.
1486 if (
1487 len(child.children) == 2
1488 and first_child.type == token.STRING
1489 and child.children[1].type == token.NEWLINE
1490 ):
1491 continue
1493 break
1495 elif first_child.type == syms.import_from:
1496 module_name = first_child.children[1]
1497 if not isinstance(module_name, Leaf) or module_name.value != "__future__":
1498 break
1500 imports |= set(get_imports_from_children(first_child.children[3:]))
1501 else:
1502 break
1504 return imports
1507def _black_info() -> str:
1508 return (
1509 f"Black {__version__} on "
1510 f"Python ({platform.python_implementation()}) {platform.python_version()}"
1511 )
1514def assert_equivalent(src: str, dst: str) -> None:
1515 """Raise AssertionError if `src` and `dst` aren't equivalent."""
1516 try:
1517 src_ast = parse_ast(src)
1518 except Exception as exc:
1519 raise ASTSafetyError(
1520 "cannot use --safe with this file; failed to parse source file AST: "
1521 f"{exc}\n"
1522 "This could be caused by running Black with an older Python version "
1523 "that does not support new syntax used in your source file."
1524 ) from exc
1526 try:
1527 dst_ast = parse_ast(dst)
1528 except Exception as exc:
1529 log = dump_to_file("".join(traceback.format_tb(exc.__traceback__)), dst)
1530 raise ASTSafetyError(
1531 f"INTERNAL ERROR: {_black_info()} produced invalid code: {exc}. "
1532 "Please report a bug on https://github.com/psf/black/issues. "
1533 f"This invalid output might be helpful: {log}"
1534 ) from None
1536 src_ast_str = "\n".join(stringify_ast(src_ast))
1537 dst_ast_str = "\n".join(stringify_ast(dst_ast))
1538 if src_ast_str != dst_ast_str:
1539 log = dump_to_file(diff(src_ast_str, dst_ast_str, "src", "dst"))
1540 raise ASTSafetyError(
1541 f"INTERNAL ERROR: {_black_info()} produced code that is not equivalent to"
1542 " the source. Please report a bug on https://github.com/psf/black/issues."
1543 f" This diff might be helpful: {log}"
1544 ) from None
1547def assert_stable(
1548 src: str, dst: str, mode: Mode, *, lines: Collection[tuple[int, int]] = ()
1549) -> None:
1550 """Raise AssertionError if `dst` reformats differently the second time."""
1551 if lines:
1552 # Formatting specified lines requires `adjusted_lines` to map original lines
1553 # to the formatted lines before re-formatting the previously formatted result.
1554 # Due to less-ideal diff algorithm, some edge cases produce incorrect new line
1555 # ranges. Hence for now, we skip the stable check.
1556 # See https://github.com/psf/black/issues/4033 for context.
1557 return
1558 # We shouldn't call format_str() here, because that formats the string
1559 # twice and may hide a bug where we bounce back and forth between two
1560 # versions.
1561 newdst = _format_str_once(dst, mode=mode, lines=lines)
1562 if dst != newdst:
1563 log = dump_to_file(
1564 str(mode),
1565 diff(src, dst, "source", "first pass"),
1566 diff(dst, newdst, "first pass", "second pass"),
1567 )
1568 raise AssertionError(
1569 f"INTERNAL ERROR: {_black_info()} produced different code on the second"
1570 " pass of the formatter. Please report a bug on"
1571 f" https://github.com/psf/black/issues. This diff might be helpful: {log}"
1572 ) from None
1575@contextmanager
1576def nullcontext() -> Iterator[None]:
1577 """Return an empty context manager.
1579 To be used like `nullcontext` in Python 3.7.
1580 """
1581 yield
1584def patched_main() -> None:
1585 # PyInstaller patches multiprocessing to need freeze_support() even in non-Windows
1586 # environments so just assume we always need to call it if frozen.
1587 if getattr(sys, "frozen", False):
1588 from multiprocessing import freeze_support
1590 freeze_support()
1592 main()
1595if __name__ == "__main__":
1596 patched_main()