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 MutableMapping,
12 Sequence,
13)
14from contextlib import nullcontext
15from dataclasses import replace
16from datetime import datetime, timezone
17from enum import Enum
18from json.decoder import JSONDecodeError
19from pathlib import Path
20from re import Pattern
21from typing import Any
23import click
24from click.core import ParameterSource
25from mypy_extensions import mypyc_attr
26from pathspec import GitIgnoreSpec
27from pathspec.patterns.gitignore import GitIgnorePatternError
29from _black_version import version as __version__
30from black.cache import Cache
31from black.comments import normalize_fmt_off
32from black.const import (
33 DEFAULT_EXCLUDES,
34 DEFAULT_INCLUDES,
35 DEFAULT_LINE_LENGTH,
36 STDIN_PLACEHOLDER,
37)
38from black.files import (
39 best_effort_relative_path,
40 find_project_root,
41 find_pyproject_toml,
42 find_user_pyproject_toml,
43 gen_python_files,
44 get_gitignore,
45 parse_pyproject_toml,
46 path_is_excluded,
47 resolves_outside_root_or_cannot_stat,
48 wrap_stream_for_windows,
49)
50from black.handle_ipynb_magics import (
51 PYTHON_CELL_MAGICS,
52 jupyter_dependencies_are_installed,
53 mask_cell,
54 put_trailing_semicolon_back,
55 remove_trailing_semicolon,
56 unmask_cell,
57 validate_cell,
58)
59from black.linegen import LN, LineGenerator, transform_line
60from black.lines import EmptyLineTracker, LinesBlock
61from black.mode import FUTURE_FLAG_TO_FEATURE, VERSION_TO_FEATURES, Feature
62from black.mode import Mode as Mode # re-exported
63from black.mode import Preview, TargetVersion, supports_feature
64from black.nodes import STARS, is_number_token, is_simple_decorator_expression, syms
65from black.output import color_diff, diff, dump_to_file, err, ipynb_diff, out
66from black.parsing import ( # noqa F401
67 ASTSafetyError,
68 InvalidInput,
69 SourceASTParseError,
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 | None, value: str | None
118) -> str | None:
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: 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 _target_versions_exceed_runtime(
207 target_versions: set[TargetVersion],
208) -> bool:
209 if not target_versions:
210 return False
211 max_target_minor = max(tv.value for tv in target_versions)
212 return max_target_minor > sys.version_info[1]
215def _version_mismatch_message(target_versions: set[TargetVersion]) -> str:
216 max_target = max(target_versions, key=lambda tv: tv.value)
217 runtime = f"{sys.version_info[0]}.{sys.version_info[1]}"
218 return (
219 f"Python {runtime} cannot parse code formatted for"
220 f" {max_target.pretty()}. To fix this: run Black with"
221 f" {max_target.pretty()}, set --target-version to"
222 f" py3{sys.version_info[1]}, or use --fast to skip the safety"
223 " check."
224 )
227def enable_unstable_feature_callback(
228 c: click.Context, p: click.Option | click.Parameter, v: tuple[str, ...]
229) -> list[Preview]:
230 """Compute the features from an --enable-unstable-feature flag."""
231 return [Preview[val] for val in v]
234def re_compile_maybe_verbose(regex: str) -> Pattern[str]:
235 """Compile a regular expression string in `regex`.
237 If it contains newlines, use verbose mode.
238 """
239 if "\n" in regex:
240 regex = "(?x)" + regex
241 compiled: Pattern[str] = re.compile(regex)
242 return compiled
245def validate_regex(
246 ctx: click.Context,
247 param: click.Parameter,
248 value: str | None,
249) -> Pattern[str] | None:
250 try:
251 return re_compile_maybe_verbose(value) if value is not None else None
252 except re.error as e:
253 raise click.BadParameter(f"Not a valid regular expression: {e}") from None
256@click.command(
257 context_settings={"help_option_names": ["-h", "--help"]},
258 # While Click does set this field automatically using the docstring, mypyc
259 # (annoyingly) strips 'em so we need to set it here too.
260 help="The uncompromising code formatter.",
261)
262@click.option("-c", "--code", type=str, help="Format the code passed in as a string.")
263@click.option(
264 "-l",
265 "--line-length",
266 type=int,
267 default=DEFAULT_LINE_LENGTH,
268 help="How many characters per line to allow.",
269 show_default=True,
270)
271@click.option(
272 "-t",
273 "--target-version",
274 type=click.Choice([v.name.lower() for v in TargetVersion]),
275 callback=target_version_option_callback,
276 multiple=True,
277 help=(
278 "Python versions that should be supported by Black's output. You should"
279 " include all versions that your code supports. By default, Black will infer"
280 " target versions from the project metadata in pyproject.toml. If this does"
281 " not yield conclusive results, Black will use per-file auto-detection."
282 ),
283)
284@click.option(
285 "--pyi",
286 is_flag=True,
287 help=(
288 "Format all input files like typing stubs regardless of file extension. This"
289 " is useful when piping source on standard input."
290 ),
291)
292@click.option(
293 "--ipynb",
294 is_flag=True,
295 help=(
296 "Format all input files like Jupyter Notebooks regardless of file extension."
297 " This is useful when piping source on standard input."
298 ),
299)
300@click.option(
301 "--python-cell-magics",
302 multiple=True,
303 help=(
304 "When processing Jupyter Notebooks, add the given magic to the list"
305 f" of known python-magics ({', '.join(sorted(PYTHON_CELL_MAGICS))})."
306 " Useful for formatting cells with custom python magics."
307 ),
308 default=[],
309)
310@click.option(
311 "-x",
312 "--skip-source-first-line",
313 is_flag=True,
314 help="Skip the first line of the source code.",
315)
316@click.option(
317 "-S",
318 "--skip-string-normalization",
319 is_flag=True,
320 help="Don't normalize string quotes or prefixes.",
321)
322@click.option(
323 "-C",
324 "--skip-magic-trailing-comma",
325 is_flag=True,
326 help="Don't use trailing commas as a reason to split lines.",
327)
328@click.option(
329 "--preview",
330 is_flag=True,
331 help=(
332 "Enable potentially disruptive style changes that may be added to Black's main"
333 " functionality in the next major release."
334 ),
335)
336@click.option(
337 "--unstable",
338 is_flag=True,
339 help=(
340 "Enable potentially disruptive style changes that have known bugs or are not"
341 " currently expected to make it into the stable style Black's next major"
342 " release. Implies --preview."
343 ),
344)
345@click.option(
346 "--enable-unstable-feature",
347 type=click.Choice([v.name for v in Preview]),
348 callback=enable_unstable_feature_callback,
349 multiple=True,
350 help=(
351 "Enable specific features included in the `--unstable` style. Requires"
352 " `--preview`. No compatibility guarantees are provided on the behavior"
353 " or existence of any unstable features."
354 ),
355)
356@click.option(
357 "--check",
358 is_flag=True,
359 help=(
360 "Don't write the files back, just return the status. Return code 0 means"
361 " nothing would change. Return code 1 means some files would be reformatted."
362 " Return code 123 means there was an internal error."
363 ),
364)
365@click.option(
366 "--diff",
367 is_flag=True,
368 help=(
369 "Don't write the files back, just output a diff to indicate what changes"
370 " Black would've made. They are printed to stdout so capturing them is simple."
371 ),
372)
373@click.option(
374 "--color/--no-color",
375 is_flag=True,
376 help="Show (or do not show) colored diff. Only applies when --diff is given.",
377)
378@click.option(
379 "--line-ranges",
380 multiple=True,
381 metavar="START-END",
382 help=(
383 "When specified, Black will try its best to only format these lines. This"
384 " option can be specified multiple times, and a union of the lines will be"
385 " formatted. Each range must be specified as two integers connected by a `-`:"
386 " `<START>-<END>`. The `<START>` and `<END>` integer indices are 1-based and"
387 " inclusive on both ends."
388 ),
389 default=(),
390)
391@click.option(
392 "--fast/--safe",
393 is_flag=True,
394 help=(
395 "By default, Black performs an AST safety check after formatting your code."
396 " The --fast flag turns off this check and the --safe flag explicitly enables"
397 " it. [default: --safe]"
398 ),
399)
400@click.option(
401 "--required-version",
402 type=str,
403 help=(
404 "Require a specific version of Black to be running. This is useful for"
405 " ensuring that all contributors to your project are using the same"
406 " version, because different versions of Black may format code a little"
407 " differently. This option can be set in a configuration file for consistent"
408 " results across environments."
409 ),
410)
411@click.option(
412 "--exclude",
413 type=str,
414 callback=validate_regex,
415 help=(
416 "A regular expression that matches files and directories that should be"
417 " excluded on recursive searches. An empty value means no paths are excluded."
418 " Use forward slashes for directories on all platforms (Windows, too)."
419 " By default, Black also ignores all paths listed in .gitignore. Changing this"
420 f" value will override all default exclusions. [default: {DEFAULT_EXCLUDES}]"
421 ),
422 show_default=False,
423)
424@click.option(
425 "--extend-exclude",
426 type=str,
427 callback=validate_regex,
428 help=(
429 "Like --exclude, but adds additional files and directories on top of the"
430 " default values instead of overriding them."
431 ),
432)
433@click.option(
434 "--force-exclude",
435 type=str,
436 callback=validate_regex,
437 help=(
438 "Like --exclude, but files and directories matching this regex will be excluded"
439 " even when they are passed explicitly as arguments. This is useful when"
440 " invoking Black programmatically on changed files, such as in a pre-commit"
441 " hook or editor plugin."
442 ),
443)
444@click.option(
445 "--stdin-filename",
446 type=str,
447 is_eager=True,
448 help=(
449 "The name of the file when passing it through stdin. Useful to make sure Black"
450 " will respect the --force-exclude option on some editors that rely on using"
451 " stdin."
452 ),
453)
454@click.option(
455 "--include",
456 type=str,
457 default=DEFAULT_INCLUDES,
458 callback=validate_regex,
459 help=(
460 "A regular expression that matches files and directories that should be"
461 " included on recursive searches. An empty value means all files are included"
462 " regardless of the name. Use forward slashes for directories on all platforms"
463 " (Windows, too). Overrides all exclusions, including from .gitignore and"
464 " command line options."
465 ),
466 show_default=True,
467)
468@click.option(
469 "-W",
470 "--workers",
471 type=click.IntRange(min=1),
472 default=None,
473 help=(
474 "When Black formats multiple files, it may use a process pool to speed up"
475 " formatting. This option controls the number of parallel workers. This can"
476 " also be specified via the BLACK_NUM_WORKERS environment variable. Defaults"
477 " to the number of CPUs in the system."
478 ),
479)
480@click.option(
481 "-q",
482 "--quiet",
483 is_flag=True,
484 help=(
485 "Stop emitting all non-critical output. Error messages will still be emitted"
486 " (which can silenced by 2>/dev/null)."
487 ),
488)
489@click.option(
490 "-v",
491 "--verbose",
492 is_flag=True,
493 help=(
494 "Emit messages about files that were not changed or were ignored due to"
495 " exclusion patterns. If Black is using a configuration file, a message"
496 " detailing which one it is using will be emitted."
497 ),
498)
499@click.version_option(
500 version=__version__,
501 message=(
502 f"%(prog)s, %(version)s (compiled: {'yes' if COMPILED else 'no'})\n"
503 f"Python ({platform.python_implementation()}) {platform.python_version()}"
504 ),
505)
506@click.argument(
507 "src",
508 nargs=-1,
509 type=click.Path(
510 exists=True, file_okay=True, dir_okay=True, readable=True, allow_dash=True
511 ),
512 is_eager=True,
513 metavar="SRC ...",
514)
515@click.option(
516 "--config",
517 type=click.Path(
518 exists=True,
519 file_okay=True,
520 dir_okay=False,
521 readable=True,
522 allow_dash=False,
523 path_type=str,
524 ),
525 is_eager=True,
526 callback=read_pyproject_toml,
527 help="Read configuration options from a configuration file.",
528)
529@click.option(
530 "--no-cache",
531 is_flag=True,
532 help=(
533 "Skip reading and writing the cache, forcing Black to reformat all"
534 " included files."
535 ),
536)
537@click.pass_context
538def main(
539 ctx: click.Context,
540 code: str | None,
541 line_length: int,
542 target_version: list[TargetVersion],
543 check: bool,
544 diff: bool,
545 line_ranges: Sequence[str],
546 color: bool,
547 fast: bool,
548 pyi: bool,
549 ipynb: bool,
550 python_cell_magics: Sequence[str],
551 skip_source_first_line: bool,
552 skip_string_normalization: bool,
553 skip_magic_trailing_comma: bool,
554 preview: bool,
555 unstable: bool,
556 enable_unstable_feature: list[Preview],
557 quiet: bool,
558 verbose: bool,
559 required_version: str | None,
560 include: Pattern[str],
561 exclude: Pattern[str] | None,
562 extend_exclude: Pattern[str] | None,
563 force_exclude: Pattern[str] | None,
564 stdin_filename: str | None,
565 workers: int | None,
566 src: tuple[str, ...],
567 config: str | None,
568 no_cache: bool,
569) -> None:
570 """The uncompromising code formatter."""
571 ctx.ensure_object(dict)
573 assert sys.version_info >= (3, 10), "Black requires Python 3.10+"
574 if sys.version_info[:3] == (3, 12, 5):
575 out(
576 "Python 3.12.5 has a memory safety issue that can cause Black's "
577 "AST safety checks to fail. "
578 "Please upgrade to Python 3.12.6 or downgrade to Python 3.12.4"
579 )
580 ctx.exit(1)
582 if src and code is not None:
583 out(
584 main.get_usage(ctx)
585 + "\n\n'SRC' and 'code' cannot be passed simultaneously."
586 )
587 ctx.exit(1)
588 if not src and code is None:
589 out(main.get_usage(ctx) + "\n\nOne of 'SRC' or 'code' is required.")
590 ctx.exit(1)
592 # It doesn't do anything if --unstable is also passed, so just allow it.
593 if enable_unstable_feature and not (preview or unstable):
594 out(
595 main.get_usage(ctx)
596 + "\n\n'--enable-unstable-feature' requires '--preview'."
597 )
598 ctx.exit(1)
600 root, method = (
601 find_project_root(src, stdin_filename) if code is None else (None, None)
602 )
603 ctx.obj["root"] = root
605 if verbose:
606 if root:
607 out(
608 f"Identified `{root}` as project root containing a {method}.",
609 fg="blue",
610 )
612 if config:
613 config_source = ctx.get_parameter_source("config")
614 user_level_config = str(find_user_pyproject_toml())
615 if config == user_level_config:
616 out(
617 "Using configuration from user-level config at "
618 f"'{user_level_config}'.",
619 fg="blue",
620 )
621 elif config_source in (
622 ParameterSource.DEFAULT,
623 ParameterSource.DEFAULT_MAP,
624 ):
625 out("Using configuration from project root.", fg="blue")
626 else:
627 out(f"Using configuration in '{config}'.", fg="blue")
628 if ctx.default_map:
629 for param, value in ctx.default_map.items():
630 out(f"{param}: {value}")
632 error_msg = "Oh no! 💥 💔 💥"
633 if (
634 required_version
635 and required_version != __version__
636 and required_version != __version__.split(".")[0]
637 ):
638 err(
639 f"{error_msg} The required version `{required_version}` does not match"
640 f" the running version `{__version__}`!"
641 )
642 ctx.exit(1)
643 if ipynb and pyi:
644 err("Cannot pass both `pyi` and `ipynb` flags!")
645 ctx.exit(1)
647 write_back = WriteBack.from_configuration(check=check, diff=diff, color=color)
648 if target_version:
649 versions = set(target_version)
650 else:
651 # We'll autodetect later.
652 versions = set()
653 mode = Mode(
654 target_versions=versions,
655 line_length=line_length,
656 is_pyi=pyi,
657 is_ipynb=ipynb,
658 skip_source_first_line=skip_source_first_line,
659 string_normalization=not skip_string_normalization,
660 magic_trailing_comma=not skip_magic_trailing_comma,
661 preview=preview,
662 unstable=unstable,
663 python_cell_magics=set(python_cell_magics),
664 enabled_features=set(enable_unstable_feature),
665 )
667 if not fast and _target_versions_exceed_runtime(versions):
668 err(
669 f"Warning: {_version_mismatch_message(versions)} Black's safety"
670 " check verifies equivalence by parsing the AST, which fails"
671 " when the running Python is older than the target version.",
672 fg="yellow",
673 )
675 lines: list[tuple[int, int]] = []
676 if line_ranges:
677 if ipynb:
678 err("Cannot use --line-ranges with ipynb files.")
679 ctx.exit(1)
681 try:
682 lines = parse_line_ranges(line_ranges)
683 except ValueError as e:
684 err(str(e))
685 ctx.exit(1)
687 if code is not None:
688 # Run in quiet mode by default with -c; the extra output isn't useful.
689 # You can still pass -v to get verbose output.
690 quiet = True
692 report = Report(check=check, diff=diff, quiet=quiet, verbose=verbose)
694 if code is not None:
695 reformat_code(
696 content=code,
697 fast=fast,
698 write_back=write_back,
699 mode=mode,
700 report=report,
701 lines=lines,
702 )
703 else:
704 assert root is not None # root is only None if code is not None
705 try:
706 sources = get_sources(
707 root=root,
708 src=src,
709 quiet=quiet,
710 verbose=verbose,
711 include=include,
712 exclude=exclude,
713 extend_exclude=extend_exclude,
714 force_exclude=force_exclude,
715 report=report,
716 stdin_filename=stdin_filename,
717 )
718 except GitIgnorePatternError:
719 ctx.exit(1)
721 if not sources:
722 if verbose or not quiet:
723 out("No Python files are present to be formatted. Nothing to do 😴")
724 if "-" in src:
725 sys.stdout.write(sys.stdin.read())
726 ctx.exit(0)
728 if len(sources) == 1:
729 reformat_one(
730 src=sources.pop(),
731 fast=fast,
732 write_back=write_back,
733 mode=mode,
734 report=report,
735 lines=lines,
736 no_cache=no_cache,
737 )
738 else:
739 from black.concurrency import reformat_many
741 if lines:
742 err("Cannot use --line-ranges to format multiple files.")
743 ctx.exit(1)
744 reformat_many(
745 sources=sources,
746 fast=fast,
747 write_back=write_back,
748 mode=mode,
749 report=report,
750 workers=workers,
751 no_cache=no_cache,
752 )
754 if verbose or not quiet:
755 if code is None and (verbose or report.change_count or report.failure_count):
756 out()
757 out(error_msg if report.return_code else "All done! ✨ 🍰 ✨")
758 if code is None:
759 click.echo(str(report), err=True)
760 ctx.exit(report.return_code)
763def get_sources(
764 *,
765 root: Path,
766 src: tuple[str, ...],
767 quiet: bool,
768 verbose: bool,
769 include: Pattern[str],
770 exclude: Pattern[str] | None,
771 extend_exclude: Pattern[str] | None,
772 force_exclude: Pattern[str] | None,
773 report: "Report",
774 stdin_filename: str | None,
775) -> set[Path]:
776 """Compute the set of files to be formatted."""
777 sources: set[Path] = set()
779 assert root.is_absolute(), f"INTERNAL ERROR: `root` must be absolute but is {root}"
780 using_default_exclude = exclude is None
781 exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES) if exclude is None else exclude
782 gitignore: dict[Path, GitIgnoreSpec] | None = None
783 root_gitignore = get_gitignore(root)
785 for s in src:
786 if s == "-" and stdin_filename:
787 path = Path(stdin_filename)
788 if path_is_excluded(stdin_filename, force_exclude):
789 report.path_ignored(
790 path,
791 "--stdin-filename matches the --force-exclude regular expression",
792 )
793 continue
794 is_stdin = True
795 else:
796 path = Path(s)
797 is_stdin = False
799 # Compare the logic here to the logic in `gen_python_files`.
800 if is_stdin or path.is_file():
801 if resolves_outside_root_or_cannot_stat(path, root, report):
802 if verbose:
803 out(f'Skipping invalid source: "{path}"', fg="red")
804 continue
806 root_relative_path = best_effort_relative_path(path, root).as_posix()
807 root_relative_path = "/" + root_relative_path
809 # Hard-exclude any files that matches the `--force-exclude` regex.
810 if path_is_excluded(root_relative_path, force_exclude):
811 report.path_ignored(
812 path, "matches the --force-exclude regular expression"
813 )
814 continue
816 if is_stdin:
817 path = Path(f"{STDIN_PLACEHOLDER}{path}")
819 if path.suffix == ".ipynb" and not jupyter_dependencies_are_installed(
820 warn=verbose or not quiet
821 ):
822 continue
824 if verbose:
825 out(f'Found input source: "{path}"', fg="blue")
826 sources.add(path)
827 elif path.is_dir():
828 path = root / (path.resolve().relative_to(root))
829 if verbose:
830 out(f'Found input source directory: "{path}"', fg="blue")
832 if using_default_exclude:
833 gitignore = {
834 root: root_gitignore,
835 path: get_gitignore(path),
836 }
837 sources.update(
838 gen_python_files(
839 path.iterdir(),
840 root,
841 include,
842 exclude,
843 extend_exclude,
844 force_exclude,
845 report,
846 gitignore,
847 verbose=verbose,
848 quiet=quiet,
849 )
850 )
851 elif s == "-":
852 if verbose:
853 out("Found input source stdin", fg="blue")
854 sources.add(path)
855 else:
856 err(f"invalid path: {s}")
858 return sources
861def reformat_code(
862 content: str,
863 fast: bool,
864 write_back: WriteBack,
865 mode: Mode,
866 report: Report,
867 *,
868 lines: Collection[tuple[int, int]] = (),
869) -> None:
870 """
871 Reformat and print out `content` without spawning child processes.
872 Similar to `reformat_one`, but for string content.
874 `fast`, `write_back`, and `mode` options are passed to
875 :func:`format_file_in_place` or :func:`format_stdin_to_stdout`.
876 """
877 path = Path("<string>")
878 try:
879 changed = Changed.NO
880 if format_stdin_to_stdout(
881 content=content, fast=fast, write_back=write_back, mode=mode, lines=lines
882 ):
883 changed = Changed.YES
884 report.done(path, changed)
885 except Exception as exc:
886 if report.verbose:
887 traceback.print_exc()
888 report.failed(path, str(exc))
891# diff-shades depends on being to monkeypatch this function to operate. I know it's
892# not ideal, but this shouldn't cause any issues ... hopefully. ~ichard26
893@mypyc_attr(patchable=True)
894def reformat_one(
895 src: Path,
896 fast: bool,
897 write_back: WriteBack,
898 mode: Mode,
899 report: "Report",
900 *,
901 lines: Collection[tuple[int, int]] = (),
902 no_cache: bool = False,
903) -> None:
904 """Reformat a single file under `src` without spawning child processes.
906 `fast`, `write_back`, and `mode` options are passed to
907 :func:`format_file_in_place` or :func:`format_stdin_to_stdout`.
908 """
909 try:
910 changed = Changed.NO
912 if str(src) == "-":
913 is_stdin = True
914 elif str(src).startswith(STDIN_PLACEHOLDER):
915 is_stdin = True
916 # Use the original name again in case we want to print something
917 # to the user
918 src = Path(str(src)[len(STDIN_PLACEHOLDER) :])
919 else:
920 is_stdin = False
922 if is_stdin:
923 if src.suffix == ".pyi":
924 mode = replace(mode, is_pyi=True)
925 elif src.suffix == ".ipynb":
926 mode = replace(mode, is_ipynb=True)
927 if format_stdin_to_stdout(
928 fast=fast, write_back=write_back, mode=mode, lines=lines
929 ):
930 changed = Changed.YES
931 else:
932 cache = None if no_cache else Cache.read(mode)
933 if cache is not None and write_back not in (
934 WriteBack.DIFF,
935 WriteBack.COLOR_DIFF,
936 ):
937 if not cache.is_changed(src):
938 changed = Changed.CACHED
939 if changed is not Changed.CACHED and format_file_in_place(
940 src, fast=fast, write_back=write_back, mode=mode, lines=lines
941 ):
942 changed = Changed.YES
943 if cache is not None and (
944 (write_back is WriteBack.YES and changed is not Changed.CACHED)
945 or (write_back is WriteBack.CHECK and changed is Changed.NO)
946 ):
947 cache.write([src])
948 report.done(src, changed)
949 except Exception as exc:
950 if report.verbose:
951 traceback.print_exc()
952 report.failed(src, str(exc))
955def format_file_in_place(
956 src: Path,
957 fast: bool,
958 mode: Mode,
959 write_back: WriteBack = WriteBack.NO,
960 lock: Any = None, # multiprocessing.Manager().Lock() is some crazy proxy
961 *,
962 lines: Collection[tuple[int, int]] = (),
963) -> bool:
964 """Format file under `src` path. Return True if changed.
966 If `write_back` is DIFF, write a diff to stdout. If it is YES, write reformatted
967 code to the file.
968 `mode` and `fast` options are passed to :func:`format_file_contents`.
969 """
970 if src.suffix == ".pyi":
971 mode = replace(mode, is_pyi=True)
972 elif src.suffix == ".ipynb":
973 mode = replace(mode, is_ipynb=True)
975 then = datetime.fromtimestamp(src.stat().st_mtime, timezone.utc)
976 header = b""
977 with open(src, "rb") as buf:
978 if mode.skip_source_first_line:
979 header = buf.readline()
980 src_contents, encoding, newline = decode_bytes(buf.read(), mode)
981 try:
982 dst_contents = format_file_contents(
983 src_contents, fast=fast, mode=mode, lines=lines
984 )
985 except NothingChanged:
986 return False
987 except JSONDecodeError:
988 raise ValueError(
989 f"File '{src}' cannot be parsed as valid Jupyter notebook."
990 ) from None
991 src_contents = header.decode(encoding) + src_contents
992 dst_contents = header.decode(encoding) + dst_contents
994 if write_back == WriteBack.YES:
995 with open(src, "w", encoding=encoding, newline=newline) as f:
996 f.write(dst_contents)
997 elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
998 now = datetime.now(timezone.utc)
999 src_name = f"{src}\t{then}"
1000 dst_name = f"{src}\t{now}"
1001 if mode.is_ipynb:
1002 diff_contents = ipynb_diff(src_contents, dst_contents, src_name, dst_name)
1003 else:
1004 diff_contents = diff(src_contents, dst_contents, src_name, dst_name)
1006 if write_back == WriteBack.COLOR_DIFF:
1007 diff_contents = color_diff(diff_contents)
1009 with lock or nullcontext():
1010 f = io.TextIOWrapper(
1011 sys.stdout.buffer,
1012 encoding=encoding,
1013 newline=newline,
1014 write_through=True,
1015 )
1016 f = wrap_stream_for_windows(f)
1017 f.write(diff_contents)
1018 f.detach()
1020 return True
1023def format_stdin_to_stdout(
1024 fast: bool,
1025 *,
1026 content: str | None = None,
1027 write_back: WriteBack = WriteBack.NO,
1028 mode: Mode,
1029 lines: Collection[tuple[int, int]] = (),
1030) -> bool:
1031 """Format file on stdin. Return True if changed.
1033 If content is None, it's read from sys.stdin.
1035 If `write_back` is YES, write reformatted code back to stdout. If it is DIFF,
1036 write a diff to stdout. The `mode` argument is passed to
1037 :func:`format_file_contents`.
1038 """
1039 then = datetime.now(timezone.utc)
1041 if content is None:
1042 src, encoding, newline = decode_bytes(sys.stdin.buffer.read(), mode)
1043 else:
1044 src, encoding, newline = content, "utf-8", "\n"
1046 dst = src
1047 try:
1048 dst = format_file_contents(src, fast=fast, mode=mode, lines=lines)
1049 return True
1051 except NothingChanged:
1052 return False
1054 finally:
1055 f = io.TextIOWrapper(
1056 sys.stdout.buffer, encoding=encoding, newline=newline, write_through=True
1057 )
1058 if write_back == WriteBack.YES:
1059 # Make sure there's a newline after the content
1060 if dst and dst[-1] != "\n" and dst[-1] != "\r":
1061 dst += newline
1062 f.write(dst)
1063 elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
1064 now = datetime.now(timezone.utc)
1065 src_name = f"STDIN\t{then}"
1066 dst_name = f"STDOUT\t{now}"
1067 d = diff(src, dst, src_name, dst_name)
1068 if write_back == WriteBack.COLOR_DIFF:
1069 d = color_diff(d)
1070 f = wrap_stream_for_windows(f)
1071 f.write(d)
1072 f.detach()
1075def check_stability_and_equivalence(
1076 src_contents: str,
1077 dst_contents: str,
1078 *,
1079 mode: Mode,
1080 lines: Collection[tuple[int, int]] = (),
1081) -> None:
1082 """Perform stability and equivalence checks.
1084 Raise AssertionError if source and destination contents are not
1085 equivalent, or if a second pass of the formatter would format the
1086 content differently.
1087 """
1088 try:
1089 assert_equivalent(src_contents, dst_contents)
1090 except SourceASTParseError:
1091 raise
1092 except ASTSafetyError:
1093 if _target_versions_exceed_runtime(mode.target_versions):
1094 raise ASTSafetyError(
1095 "failed to verify equivalence of the formatted output:"
1096 f" {_version_mismatch_message(mode.target_versions)}"
1097 ) from None
1098 raise
1099 assert_stable(src_contents, dst_contents, mode=mode, lines=lines)
1102def format_file_contents(
1103 src_contents: str,
1104 *,
1105 fast: bool,
1106 mode: Mode,
1107 lines: Collection[tuple[int, int]] = (),
1108) -> FileContent:
1109 """Reformat contents of a file and return new contents.
1111 If `fast` is False, additionally confirm that the reformatted code is
1112 valid by calling :func:`assert_equivalent` and :func:`assert_stable` on it.
1113 `mode` is passed to :func:`format_str`.
1114 """
1115 if mode.is_ipynb:
1116 dst_contents = format_ipynb_string(src_contents, fast=fast, mode=mode)
1117 else:
1118 dst_contents = format_str(src_contents, mode=mode, lines=lines)
1119 if src_contents == dst_contents:
1120 raise NothingChanged
1122 if not fast and not mode.is_ipynb:
1123 # Jupyter notebooks will already have been checked above.
1124 check_stability_and_equivalence(
1125 src_contents, dst_contents, mode=mode, lines=lines
1126 )
1127 return dst_contents
1130def format_cell(src: str, *, fast: bool, mode: Mode) -> str:
1131 """Format code in given cell of Jupyter notebook.
1133 General idea is:
1135 - if cell has trailing semicolon, remove it;
1136 - if cell has IPython magics, mask them;
1137 - format cell;
1138 - reinstate IPython magics;
1139 - reinstate trailing semicolon (if originally present);
1140 - strip trailing newlines.
1142 Cells with syntax errors will not be processed, as they
1143 could potentially be automagics or multi-line magics, which
1144 are currently not supported.
1145 """
1146 validate_cell(src, mode)
1147 src_without_trailing_semicolon, has_trailing_semicolon = remove_trailing_semicolon(
1148 src
1149 )
1150 try:
1151 masked_src, replacements = mask_cell(src_without_trailing_semicolon)
1152 except SyntaxError:
1153 raise NothingChanged from None
1154 masked_dst = format_str(masked_src, mode=mode)
1155 if not fast:
1156 check_stability_and_equivalence(masked_src, masked_dst, mode=mode)
1157 dst_without_trailing_semicolon = unmask_cell(masked_dst, replacements)
1158 dst = put_trailing_semicolon_back(
1159 dst_without_trailing_semicolon, has_trailing_semicolon
1160 )
1161 dst = dst.rstrip("\n")
1162 if dst == src:
1163 raise NothingChanged from None
1164 return dst
1167def validate_metadata(nb: MutableMapping[str, Any]) -> None:
1168 """If notebook is marked as non-Python, don't format it.
1170 All notebook metadata fields are optional, see
1171 https://nbformat.readthedocs.io/en/latest/format_description.html. So
1172 if a notebook has empty metadata, we will try to parse it anyway.
1173 """
1174 language = nb.get("metadata", {}).get("language_info", {}).get("name", None)
1175 if language is not None and language != "python":
1176 raise NothingChanged from None
1179def format_ipynb_string(src_contents: str, *, fast: bool, mode: Mode) -> FileContent:
1180 """Format Jupyter notebook.
1182 Operate cell-by-cell, only on code cells, only for Python notebooks.
1183 If the ``.ipynb`` originally had a trailing newline, it'll be preserved.
1184 """
1185 if not src_contents:
1186 raise NothingChanged
1188 trailing_newline = src_contents[-1] == "\n"
1189 modified = False
1190 nb = json.loads(src_contents)
1191 validate_metadata(nb)
1192 for cell in nb["cells"]:
1193 if cell.get("cell_type", None) == "code":
1194 try:
1195 src = "".join(cell["source"])
1196 dst = format_cell(src, fast=fast, mode=mode)
1197 except NothingChanged:
1198 pass
1199 else:
1200 cell["source"] = dst.splitlines(keepends=True)
1201 modified = True
1202 if modified:
1203 dst_contents = json.dumps(nb, indent=1, ensure_ascii=False)
1204 if trailing_newline:
1205 dst_contents = dst_contents + "\n"
1206 return dst_contents
1207 else:
1208 raise NothingChanged
1211def format_str(
1212 src_contents: str, *, mode: Mode, lines: Collection[tuple[int, int]] = ()
1213) -> str:
1214 """Reformat a string and return new contents.
1216 `mode` determines formatting options, such as how many characters per line are
1217 allowed. Example:
1219 >>> import black
1220 >>> print(black.format_str("def f(arg:str='')->None:...", mode=black.Mode()))
1221 def f(arg: str = "") -> None:
1222 ...
1224 A more complex example:
1226 >>> print(
1227 ... black.format_str(
1228 ... "def f(arg:str='')->None: hey",
1229 ... mode=black.Mode(
1230 ... target_versions={black.TargetVersion.PY36},
1231 ... line_length=10,
1232 ... string_normalization=False,
1233 ... is_pyi=False,
1234 ... ),
1235 ... ),
1236 ... )
1237 def f(
1238 arg: str = '',
1239 ) -> None:
1240 hey
1242 """
1243 if lines:
1244 lines = sanitized_lines(lines, src_contents)
1245 if not lines:
1246 return src_contents # Nothing to format
1247 dst_contents = _format_str_once(src_contents, mode=mode, lines=lines)
1248 # Forced second pass to work around optional trailing commas (becoming
1249 # forced trailing commas on pass 2) interacting differently with optional
1250 # parentheses. Admittedly ugly.
1251 if src_contents != dst_contents:
1252 if lines:
1253 lines = adjusted_lines(lines, src_contents, dst_contents)
1254 return _format_str_once(dst_contents, mode=mode, lines=lines)
1255 return dst_contents
1258def _format_str_once(
1259 src_contents: str, *, mode: Mode, lines: Collection[tuple[int, int]] = ()
1260) -> str:
1261 # Use the encoding overwrite since the src_contents may contain a different
1262 # magic encoding comment than utf-8
1263 normalized_contents, _, newline_type = decode_bytes(
1264 src_contents.encode("utf-8"), mode, encoding_overwrite="utf-8"
1265 )
1267 src_node = lib2to3_parse(
1268 normalized_contents.lstrip(), target_versions=mode.target_versions
1269 )
1271 dst_blocks: list[LinesBlock] = []
1272 if mode.target_versions:
1273 versions = mode.target_versions
1274 else:
1275 future_imports = get_future_imports(src_node)
1276 versions = detect_target_versions(src_node, future_imports=future_imports)
1278 line_generation_features = {
1279 feature
1280 for feature in {
1281 Feature.PARENTHESIZED_CONTEXT_MANAGERS,
1282 Feature.UNPARENTHESIZED_EXCEPT_TYPES,
1283 Feature.T_STRINGS,
1284 }
1285 if supports_feature(versions, feature)
1286 }
1287 normalize_fmt_off(src_node, mode, lines)
1288 if lines:
1289 # This should be called after normalize_fmt_off.
1290 convert_unchanged_lines(src_node, lines)
1292 line_generator = LineGenerator(mode=mode, features=line_generation_features)
1293 elt = EmptyLineTracker(mode=mode)
1294 split_line_features = {
1295 feature
1296 for feature in {
1297 Feature.TRAILING_COMMA_IN_CALL,
1298 Feature.TRAILING_COMMA_IN_DEF,
1299 }
1300 if supports_feature(versions, feature)
1301 }
1302 block: LinesBlock | None = None
1303 for current_line in line_generator.visit(src_node):
1304 block = elt.maybe_empty_lines(current_line)
1305 dst_blocks.append(block)
1306 for line in transform_line(
1307 current_line, mode=mode, features=split_line_features
1308 ):
1309 block.content_lines.append(str(line))
1310 if dst_blocks:
1311 dst_blocks[-1].after = 0
1312 dst_contents = []
1313 for block in dst_blocks:
1314 dst_contents.extend(block.all_lines())
1315 if not dst_contents:
1316 if "\n" in normalized_contents:
1317 return newline_type
1318 return "".join(dst_contents).replace("\n", newline_type)
1321def decode_bytes(
1322 src: bytes, mode: Mode, *, encoding_overwrite: str | None = None
1323) -> tuple[FileContent, Encoding, NewLine]:
1324 """Return a tuple of (decoded_contents, encoding, newline).
1326 `newline` is either CRLF, LF, or CR, but `decoded_contents` is decoded with
1327 universal newlines (i.e. only contains LF).
1329 Use the keyword only encoding_overwrite argument if the bytes are encoded
1330 differently to their possible encoding magic comment.
1331 """
1332 srcbuf = io.BytesIO(src)
1334 # Still use detect encoding even if overwrite set because otherwise lines
1335 # might be different
1336 encoding, lines = tokenize.detect_encoding(srcbuf.readline)
1337 if encoding_overwrite is not None:
1338 encoding = encoding_overwrite
1340 if not lines:
1341 return "", encoding, "\n"
1343 if lines[0][-2:] == b"\r\n":
1344 if b"\r" in lines[0][:-2]:
1345 newline = "\r"
1346 else:
1347 newline = "\r\n"
1348 elif lines[0][-1:] == b"\n":
1349 if b"\r" in lines[0][:-1]:
1350 newline = "\r"
1351 else:
1352 newline = "\n"
1353 else:
1354 if b"\r" in lines[0]:
1355 newline = "\r"
1356 else:
1357 newline = "\n"
1359 srcbuf.seek(0)
1360 with io.TextIOWrapper(srcbuf, encoding) as tiow:
1361 return tiow.read(), encoding, newline
1364def get_features_used(
1365 node: Node, *, future_imports: set[str] | None = None
1366) -> set[Feature]:
1367 """Return a set of (relatively) new Python features used in this file.
1369 Currently looking for:
1370 - f-strings;
1371 - self-documenting expressions in f-strings (f"{x=}");
1372 - underscores in numeric literals;
1373 - trailing commas after * or ** in function signatures and calls;
1374 - positional only arguments in function signatures and lambdas;
1375 - assignment expression;
1376 - relaxed decorator syntax;
1377 - usage of __future__ flags (annotations);
1378 - print / exec statements;
1379 - parenthesized context managers;
1380 - match statements;
1381 - except* clause;
1382 - variadic generics;
1383 - lazy imports;
1384 - starred or double-starred comprehensions.
1385 """
1386 features: set[Feature] = set()
1387 if future_imports:
1388 features |= {
1389 FUTURE_FLAG_TO_FEATURE[future_import]
1390 for future_import in future_imports
1391 if future_import in FUTURE_FLAG_TO_FEATURE
1392 }
1394 for n in node.pre_order():
1395 if n.type == token.FSTRING_START:
1396 features.add(Feature.F_STRINGS)
1397 elif n.type == token.TSTRING_START:
1398 features.add(Feature.T_STRINGS)
1399 elif (
1400 n.type == token.RBRACE
1401 and n.parent is not None
1402 and any(child.type == token.EQUAL for child in n.parent.children)
1403 ):
1404 features.add(Feature.DEBUG_F_STRINGS)
1406 elif is_number_token(n):
1407 if "_" in n.value:
1408 features.add(Feature.NUMERIC_UNDERSCORES)
1410 elif n.type == token.SLASH:
1411 if n.parent and n.parent.type in {
1412 syms.typedargslist,
1413 syms.arglist,
1414 syms.varargslist,
1415 }:
1416 features.add(Feature.POS_ONLY_ARGUMENTS)
1418 elif n.type == token.COLONEQUAL:
1419 features.add(Feature.ASSIGNMENT_EXPRESSIONS)
1421 elif n.type == token.LAZY:
1422 features.add(Feature.LAZY_IMPORTS)
1424 elif n.type == syms.decorator:
1425 if len(n.children) > 1 and not is_simple_decorator_expression(
1426 n.children[1]
1427 ):
1428 features.add(Feature.RELAXED_DECORATORS)
1430 elif is_unpacking_comprehension(n):
1431 features.add(Feature.UNPACKING_IN_COMPREHENSIONS)
1433 elif (
1434 n.type in {syms.typedargslist, syms.arglist}
1435 and n.children
1436 and n.children[-1].type == token.COMMA
1437 ):
1438 if n.type == syms.typedargslist:
1439 feature = Feature.TRAILING_COMMA_IN_DEF
1440 else:
1441 feature = Feature.TRAILING_COMMA_IN_CALL
1443 for ch in n.children:
1444 if ch.type in STARS:
1445 features.add(feature)
1447 if ch.type == syms.argument:
1448 for argch in ch.children:
1449 if argch.type in STARS:
1450 features.add(feature)
1452 elif (
1453 n.type in {syms.return_stmt, syms.yield_expr}
1454 and len(n.children) >= 2
1455 and n.children[1].type == syms.testlist_star_expr
1456 and any(child.type == syms.star_expr for child in n.children[1].children)
1457 ):
1458 features.add(Feature.UNPACKING_ON_FLOW)
1460 elif (
1461 n.type == syms.annassign
1462 and len(n.children) >= 4
1463 and n.children[3].type == syms.testlist_star_expr
1464 ):
1465 features.add(Feature.ANN_ASSIGN_EXTENDED_RHS)
1467 elif (
1468 n.type == syms.with_stmt
1469 and len(n.children) > 2
1470 and n.children[1].type == syms.atom
1471 ):
1472 atom_children = n.children[1].children
1473 if (
1474 len(atom_children) == 3
1475 and atom_children[0].type == token.LPAR
1476 and _contains_asexpr(atom_children[1])
1477 and atom_children[2].type == token.RPAR
1478 ):
1479 features.add(Feature.PARENTHESIZED_CONTEXT_MANAGERS)
1481 elif n.type == syms.match_stmt:
1482 features.add(Feature.PATTERN_MATCHING)
1484 elif n.type in {syms.subscriptlist, syms.trailer} and any(
1485 child.type == syms.star_expr for child in n.children
1486 ):
1487 features.add(Feature.VARIADIC_GENERICS)
1489 elif (
1490 n.type == syms.tname_star
1491 and len(n.children) == 3
1492 and n.children[2].type == syms.star_expr
1493 ):
1494 features.add(Feature.VARIADIC_GENERICS)
1496 elif n.type in (syms.type_stmt, syms.typeparams):
1497 features.add(Feature.TYPE_PARAMS)
1499 elif (
1500 n.type in (syms.typevartuple, syms.paramspec, syms.typevar)
1501 and n.children[-2].type == token.EQUAL
1502 ):
1503 features.add(Feature.TYPE_PARAM_DEFAULTS)
1505 elif (
1506 n.type == syms.except_clause
1507 and len(n.children) >= 2
1508 and (
1509 n.children[1].type == token.STAR or n.children[1].type == syms.testlist
1510 )
1511 ):
1512 is_star_except = n.children[1].type == token.STAR
1514 if is_star_except:
1515 features.add(Feature.EXCEPT_STAR)
1517 # Presence of except* pushes as clause 1 index back
1518 has_as_clause = (
1519 len(n.children) >= is_star_except + 3
1520 and n.children[is_star_except + 2].type == token.NAME
1521 and n.children[is_star_except + 2].value == "as" # type: ignore
1522 )
1524 # If there's no 'as' clause and the except expression is a testlist.
1525 if not has_as_clause and (
1526 (is_star_except and n.children[2].type == syms.testlist)
1527 or (not is_star_except and n.children[1].type == syms.testlist)
1528 ):
1529 features.add(Feature.UNPARENTHESIZED_EXCEPT_TYPES)
1531 return features
1534def is_unpacking_comprehension(node: LN) -> bool:
1535 if node.type not in {syms.listmaker, syms.testlist_gexp, syms.dictsetmaker}:
1536 return False
1538 if not any(
1539 child.type in {syms.comp_for, syms.old_comp_for} for child in node.children
1540 ):
1541 return False
1543 first_child = node.children[0]
1544 return first_child.type == syms.star_expr or first_child.type == token.DOUBLESTAR
1547def _contains_asexpr(node: Node | Leaf) -> bool:
1548 """Return True if `node` contains an as-pattern."""
1549 if node.type == syms.asexpr_test:
1550 return True
1551 elif node.type == syms.atom:
1552 if (
1553 len(node.children) == 3
1554 and node.children[0].type == token.LPAR
1555 and node.children[2].type == token.RPAR
1556 ):
1557 return _contains_asexpr(node.children[1])
1558 elif node.type == syms.testlist_gexp:
1559 return any(_contains_asexpr(child) for child in node.children)
1560 return False
1563def detect_target_versions(
1564 node: Node, *, future_imports: set[str] | None = None
1565) -> set[TargetVersion]:
1566 """Detect the version to target based on the nodes used."""
1567 features = get_features_used(node, future_imports=future_imports)
1568 return {
1569 version for version in TargetVersion if features <= VERSION_TO_FEATURES[version]
1570 }
1573def get_future_imports(node: Node) -> set[str]:
1574 """Return a set of __future__ imports in the file."""
1575 imports: set[str] = set()
1577 def get_imports_from_children(children: list[LN]) -> Generator[str, None, None]:
1578 for child in children:
1579 if isinstance(child, Leaf):
1580 if child.type == token.NAME:
1581 yield child.value
1583 elif child.type == syms.import_as_name:
1584 orig_name = child.children[0]
1585 assert isinstance(orig_name, Leaf), "Invalid syntax parsing imports"
1586 assert orig_name.type == token.NAME, "Invalid syntax parsing imports"
1587 yield orig_name.value
1589 elif child.type == syms.import_as_names:
1590 yield from get_imports_from_children(child.children)
1592 else:
1593 raise AssertionError("Invalid syntax parsing imports")
1595 for child in node.children:
1596 if child.type != syms.simple_stmt:
1597 break
1599 first_child = child.children[0]
1600 if isinstance(first_child, Leaf):
1601 # Continue looking if we see a docstring; otherwise stop.
1602 if (
1603 len(child.children) == 2
1604 and first_child.type == token.STRING
1605 and child.children[1].type == token.NEWLINE
1606 ):
1607 continue
1609 break
1611 elif first_child.type == syms.import_from:
1612 if first_child.children[0].type == token.LAZY:
1613 break
1615 module_name = first_child.children[1]
1616 if not isinstance(module_name, Leaf) or module_name.value != "__future__":
1617 break
1619 imports |= set(get_imports_from_children(first_child.children[3:]))
1620 else:
1621 break
1623 return imports
1626def _black_info() -> str:
1627 return (
1628 f"Black {__version__} on "
1629 f"Python ({platform.python_implementation()}) {platform.python_version()}"
1630 )
1633def assert_equivalent(src: str, dst: str) -> None:
1634 """Raise AssertionError if `src` and `dst` aren't equivalent."""
1635 try:
1636 src_ast = parse_ast(src)
1637 except Exception as exc:
1638 raise SourceASTParseError(
1639 "cannot use --safe with this file; failed to parse source file AST: "
1640 f"{exc}\n"
1641 "This could be caused by running Black with an older Python version "
1642 "that does not support new syntax used in your source file."
1643 ) from exc
1645 try:
1646 dst_ast = parse_ast(dst)
1647 except Exception as exc:
1648 log = dump_to_file("".join(traceback.format_tb(exc.__traceback__)), dst)
1649 raise ASTSafetyError(
1650 f"INTERNAL ERROR: {_black_info()} produced invalid code: {exc}. "
1651 "Please report a bug on https://github.com/psf/black/issues. "
1652 f"This invalid output might be helpful: {log}"
1653 ) from None
1655 src_ast_str = "\n".join(stringify_ast(src_ast))
1656 dst_ast_str = "\n".join(stringify_ast(dst_ast))
1657 if src_ast_str != dst_ast_str:
1658 log = dump_to_file(diff(src_ast_str, dst_ast_str, "src", "dst"))
1659 raise ASTSafetyError(
1660 f"INTERNAL ERROR: {_black_info()} produced code that is not equivalent to"
1661 " the source. Please report a bug on https://github.com/psf/black/issues."
1662 f" This diff might be helpful: {log}"
1663 ) from None
1666def assert_stable(
1667 src: str, dst: str, mode: Mode, *, lines: Collection[tuple[int, int]] = ()
1668) -> None:
1669 """Raise AssertionError if `dst` reformats differently the second time."""
1670 if lines:
1671 # Formatting specified lines requires `adjusted_lines` to map original lines
1672 # to the formatted lines before re-formatting the previously formatted result.
1673 # Due to less-ideal diff algorithm, some edge cases produce incorrect new line
1674 # ranges. Hence for now, we skip the stable check.
1675 # See https://github.com/psf/black/issues/4033 for context.
1676 return
1677 # We shouldn't call format_str() here, because that formats the string
1678 # twice and may hide a bug where we bounce back and forth between two
1679 # versions.
1680 newdst = _format_str_once(dst, mode=mode, lines=lines)
1681 if dst != newdst:
1682 log = dump_to_file(
1683 str(mode),
1684 diff(src, dst, "source", "first pass"),
1685 diff(dst, newdst, "first pass", "second pass"),
1686 )
1687 raise AssertionError(
1688 f"INTERNAL ERROR: {_black_info()} produced different code on the second"
1689 " pass of the formatter. Please report a bug on"
1690 f" https://github.com/psf/black/issues. This diff might be helpful: {log}"
1691 ) from None
1694def patched_main() -> None:
1695 # PyInstaller patches multiprocessing to need freeze_support() even in non-Windows
1696 # environments so just assume we always need to call it if frozen.
1697 if getattr(sys, "frozen", False):
1698 from multiprocessing import freeze_support
1700 freeze_support()
1702 main()
1705if __name__ == "__main__":
1706 patched_main()