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