Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/black/__init__.py: 17%
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 os
4import platform
5import re
6import sys
7import tokenize
8import traceback
9from collections.abc import (
10 Collection,
11 Generator,
12 MutableMapping,
13 Sequence,
14)
15from contextlib import nullcontext
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
24import click
25from click.core import ParameterSource
26from mypy_extensions import mypyc_attr
27from pathspec import GitIgnoreSpec
28from pathspec.patterns.gitignore import GitIgnorePatternError
30import black.cache as cache_module
31from _black_version import version as __version__
32from black.cache import Cache, get_cache_dir
33from black.comments import normalize_fmt_off
34from black.const import (
35 DEFAULT_EXCLUDES,
36 DEFAULT_INCLUDES,
37 DEFAULT_LINE_LENGTH,
38 STDIN_PLACEHOLDER,
39)
40from black.files import (
41 best_effort_relative_path,
42 find_project_root,
43 find_pyproject_toml,
44 find_user_pyproject_toml,
45 gen_python_files,
46 get_gitignore,
47 parse_pyproject_toml,
48 path_is_excluded,
49 resolves_outside_root_or_cannot_stat,
50 wrap_stream_for_windows,
51)
52from black.handle_ipynb_magics import (
53 PYTHON_CELL_MAGICS,
54 jupyter_dependencies_are_installed,
55 mask_cell,
56 put_trailing_semicolon_back,
57 remove_trailing_semicolon,
58 unmask_cell,
59 validate_cell,
60)
61from black.linegen import LN, LineGenerator, transform_line
62from black.lines import EmptyLineTracker, LinesBlock
63from black.mode import FUTURE_FLAG_TO_FEATURE, VERSION_TO_FEATURES, Feature
64from black.mode import Mode as Mode # re-exported
65from black.mode import Preview, TargetVersion, supports_feature
66from black.nodes import STARS, is_number_token, is_simple_decorator_expression, syms
67from black.output import color_diff, diff, dump_to_file, err, ipynb_diff, out
68from black.parsing import ( # noqa F401
69 ASTSafetyError,
70 InvalidInput,
71 SourceASTParseError,
72 lib2to3_parse,
73 parse_ast,
74 stringify_ast,
75)
76from black.ranges import (
77 adjusted_lines,
78 convert_unchanged_lines,
79 parse_line_ranges,
80 sanitized_lines,
81)
82from black.report import Changed, NothingChanged, Report
83from blib2to3 import pygram
84from blib2to3.pgen2 import token
85from blib2to3.pytree import Leaf, Node
87COMPILED = Path(__file__).suffix in (".pyd", ".so")
89# types
90FileContent = str
91Encoding = str
92NewLine = str
95class WriteBack(Enum):
96 NO = 0
97 YES = 1
98 DIFF = 2
99 CHECK = 3
100 COLOR_DIFF = 4
102 @classmethod
103 def from_configuration(
104 cls, *, check: bool, diff: bool, color: bool = False
105 ) -> "WriteBack":
106 if check and not diff:
107 return cls.CHECK
109 if diff and color:
110 return cls.COLOR_DIFF
112 return cls.DIFF if diff else cls.YES
115# Legacy name, left for integrations.
116FileMode = Mode
119def read_pyproject_toml(
120 ctx: click.Context, param: click.Parameter | None, value: str | None
121) -> str | None:
122 """Inject Black configuration from "pyproject.toml" into defaults in `ctx`.
124 Returns the path to a successfully found and read configuration file, None
125 otherwise.
126 """
127 if not value:
128 value = find_pyproject_toml(
129 ctx.params.get("src", ()), ctx.params.get("stdin_filename", None)
130 )
131 if value is None:
132 return None
134 try:
135 config = parse_pyproject_toml(value)
136 except (OSError, ValueError) as e:
137 raise click.FileError(
138 filename=value, hint=f"Error reading configuration file: {e}"
139 ) from None
141 if not config:
142 return None
143 else:
144 spellcheck_pyproject_toml_keys(ctx, list(config), value)
145 # Sanitize the values to be Click friendly. For more information please see:
146 # https://github.com/psf/black/issues/1458
147 # https://github.com/pallets/click/issues/1567
148 config = {
149 k: str(v) if not isinstance(v, (list, dict)) else v
150 for k, v in config.items()
151 }
153 target_version = config.get("target_version")
154 if target_version is not None and not isinstance(target_version, list):
155 raise click.BadOptionUsage(
156 "target-version", "Config key target-version must be a list"
157 )
159 exclude = config.get("exclude")
160 if exclude is not None and not isinstance(exclude, str):
161 raise click.BadOptionUsage("exclude", "Config key exclude must be a string")
163 include = config.get("include")
164 if include is not None and not isinstance(include, str):
165 raise click.BadOptionUsage("include", "Config key include must be a string")
167 extend_exclude = config.get("extend_exclude")
168 if extend_exclude is not None and not isinstance(extend_exclude, str):
169 raise click.BadOptionUsage(
170 "extend-exclude", "Config key extend-exclude must be a string"
171 )
173 force_exclude = config.get("force_exclude")
174 if force_exclude is not None and not isinstance(force_exclude, str):
175 raise click.BadOptionUsage(
176 "force-exclude", "Config key force-exclude must be a string"
177 )
179 line_ranges = config.get("line_ranges")
180 if line_ranges is not None:
181 raise click.BadOptionUsage(
182 "line-ranges", "Cannot use line-ranges in the pyproject.toml file."
183 )
185 default_map: dict[str, Any] = {}
186 if ctx.default_map:
187 default_map.update(ctx.default_map)
188 default_map.update(config)
190 ctx.default_map = default_map
191 return value
194def spellcheck_pyproject_toml_keys(
195 ctx: click.Context, config_keys: list[str], config_file_path: str
196) -> None:
197 invalid_keys: list[str] = []
198 available_config_options = {param.name for param in ctx.command.params}
199 invalid_keys = [key for key in config_keys if key not in available_config_options]
200 if invalid_keys:
201 keys_str = ", ".join(map(repr, invalid_keys))
202 out(
203 f"Invalid config keys detected: {keys_str} (in {config_file_path})",
204 fg="red",
205 )
208def target_version_option_callback(
209 c: click.Context, p: click.Option | click.Parameter, v: tuple[str, ...]
210) -> list[TargetVersion]:
211 """Compute the target versions from a --target-version flag.
213 This is its own function because mypy couldn't infer the type correctly
214 when it was a lambda, causing mypyc trouble.
215 """
216 return [TargetVersion[val.upper()] for val in v]
219def _target_versions_exceed_runtime(
220 target_versions: set[TargetVersion],
221) -> bool:
222 """Check if ALL target versions exceed the runtime Python version.
224 If any target version is at or below the runtime version, the AST
225 safety check can succeed for that version's features, so the warning
226 would be spurious.
227 """
228 if not target_versions:
229 return False
230 min_target_minor = min(tv.value for tv in target_versions)
231 return min_target_minor > sys.version_info[1]
234def _version_mismatch_message(target_versions: set[TargetVersion]) -> str:
235 max_target = max(target_versions, key=lambda tv: tv.value)
236 runtime = f"{sys.version_info[0]}.{sys.version_info[1]}"
237 return (
238 f"Python {runtime} cannot parse code formatted for"
239 f" {max_target.pretty()}. To fix this: run Black with"
240 f" {max_target.pretty()}, set --target-version to"
241 f" py3{sys.version_info[1]}, or use --fast to skip the safety"
242 " check."
243 )
246def enable_unstable_feature_callback(
247 c: click.Context, p: click.Option | click.Parameter, v: tuple[str, ...]
248) -> list[Preview]:
249 """Compute the features from an --enable-unstable-feature flag."""
250 return [Preview[val] for val in v]
253def re_compile_maybe_verbose(regex: str) -> Pattern[str]:
254 """Compile a regular expression string in `regex`.
256 If it contains newlines, use verbose mode.
257 """
258 if "\n" in regex:
259 regex = "(?x)" + regex
260 compiled: Pattern[str] = re.compile(regex)
261 return compiled
264def validate_regex(
265 ctx: click.Context,
266 param: click.Parameter,
267 value: str | None,
268) -> Pattern[str] | None:
269 try:
270 return re_compile_maybe_verbose(value) if value is not None else None
271 except re.error as e:
272 raise click.BadParameter(f"Not a valid regular expression: {e}") from None
275@click.command(
276 context_settings={"help_option_names": ["-h", "--help"]},
277 # While Click does set this field automatically using the docstring, mypyc
278 # (annoyingly) strips 'em so we need to set it here too.
279 help="The uncompromising code formatter.",
280)
281@click.option("-c", "--code", type=str, help="Format the code passed in as a string.")
282@click.option(
283 "-l",
284 "--line-length",
285 type=int,
286 default=DEFAULT_LINE_LENGTH,
287 help="How many characters per line to allow.",
288 show_default=True,
289)
290@click.option(
291 "-t",
292 "--target-version",
293 type=click.Choice([v.name.lower() for v in TargetVersion]),
294 callback=target_version_option_callback,
295 multiple=True,
296 help=(
297 "Python versions that should be supported by Black's output. You should"
298 " include all versions that your code supports. By default, Black will infer"
299 " target versions from the project metadata in pyproject.toml. If this does"
300 " not yield conclusive results, Black will use per-file auto-detection."
301 ),
302)
303@click.option(
304 "--pyi",
305 is_flag=True,
306 help=(
307 "Format all input files like typing stubs regardless of file extension. This"
308 " is useful when piping source on standard input."
309 ),
310)
311@click.option(
312 "--ipynb",
313 is_flag=True,
314 help=(
315 "Format all input files like Jupyter Notebooks regardless of file extension."
316 " This is useful when piping source on standard input."
317 ),
318)
319@click.option(
320 "--python-cell-magics",
321 multiple=True,
322 help=(
323 "When processing Jupyter Notebooks, add the given magic to the list"
324 f" of known python-magics ({', '.join(sorted(PYTHON_CELL_MAGICS))})."
325 " Useful for formatting cells with custom python magics."
326 ),
327 default=[],
328)
329@click.option(
330 "-x",
331 "--skip-source-first-line",
332 is_flag=True,
333 help="Skip the first line of the source code.",
334)
335@click.option(
336 "-S",
337 "--skip-string-normalization",
338 is_flag=True,
339 help="Don't normalize string quotes or prefixes.",
340)
341@click.option(
342 "-C",
343 "--skip-magic-trailing-comma",
344 is_flag=True,
345 help="Don't use trailing commas as a reason to split lines.",
346)
347@click.option(
348 "--preview",
349 is_flag=True,
350 help=(
351 "Enable potentially disruptive style changes that may be added to Black's main"
352 " functionality in the next major release."
353 ),
354)
355@click.option(
356 "--unstable",
357 is_flag=True,
358 help=(
359 "Enable potentially disruptive style changes that have known bugs or are not"
360 " currently expected to make it into the stable style Black's next major"
361 " release. Implies --preview."
362 ),
363)
364@click.option(
365 "--enable-unstable-feature",
366 type=click.Choice([v.name for v in Preview]),
367 callback=enable_unstable_feature_callback,
368 multiple=True,
369 help=(
370 "Enable specific features included in the `--unstable` style. Requires"
371 " `--preview`. No compatibility guarantees are provided on the behavior"
372 " or existence of any unstable features."
373 ),
374)
375@click.option(
376 "--check",
377 is_flag=True,
378 help=(
379 "Don't write the files back, just return the status. Return code 0 means"
380 " nothing would change. Return code 1 means some files would be reformatted."
381 " Return code 123 means there was an internal error."
382 ),
383)
384@click.option(
385 "--diff",
386 is_flag=True,
387 help=(
388 "Don't write the files back, just output a diff to indicate what changes"
389 " Black would've made. They are printed to stdout so capturing them is simple."
390 ),
391)
392@click.option(
393 "--color/--no-color",
394 is_flag=True,
395 help="Show (or do not show) colored diff. Only applies when --diff is given.",
396)
397@click.option(
398 "--line-ranges",
399 multiple=True,
400 metavar="START-END",
401 help=(
402 "When specified, Black will try its best to only format these lines. This"
403 " option can be specified multiple times, and a union of the lines will be"
404 " formatted. Each range must be specified as two integers connected by a `-`:"
405 " `<START>-<END>`. The `<START>` and `<END>` integer indices are 1-based and"
406 " inclusive on both ends."
407 ),
408 default=(),
409)
410@click.option(
411 "--fast/--safe",
412 is_flag=True,
413 help=(
414 "By default, Black performs an AST safety check after formatting your code."
415 " The --fast flag turns off this check and the --safe flag explicitly enables"
416 " it. [default: --safe]"
417 ),
418)
419@click.option(
420 "--required-version",
421 type=str,
422 help=(
423 "Require a specific version of Black to be running. This is useful for"
424 " ensuring that all contributors to your project are using the same"
425 " version, because different versions of Black may format code a little"
426 " differently. This option can be set in a configuration file for consistent"
427 " results across environments."
428 ),
429)
430@click.option(
431 "--exclude",
432 type=str,
433 callback=validate_regex,
434 help=(
435 "A regular expression that matches files and directories that should be"
436 " excluded on recursive searches. An empty value means no paths are excluded."
437 " Use forward slashes for directories on all platforms (Windows, too)."
438 " By default, Black also ignores all paths listed in .gitignore. Changing this"
439 f" value will override all default exclusions. [default: {DEFAULT_EXCLUDES}]"
440 ),
441 show_default=False,
442)
443@click.option(
444 "--extend-exclude",
445 type=str,
446 callback=validate_regex,
447 help=(
448 "Like --exclude, but adds additional files and directories on top of the"
449 " default values instead of overriding them."
450 ),
451)
452@click.option(
453 "--force-exclude",
454 type=str,
455 callback=validate_regex,
456 help=(
457 "Like --exclude, but files and directories matching this regex will be excluded"
458 " even when they are passed explicitly as arguments. This is useful when"
459 " invoking Black programmatically on changed files, such as in a pre-commit"
460 " hook or editor plugin."
461 ),
462)
463@click.option(
464 "--stdin-filename",
465 type=str,
466 is_eager=True,
467 help=(
468 "The name of the file when passing it through stdin. Useful to make sure Black"
469 " will respect the --force-exclude option on some editors that rely on using"
470 " stdin."
471 ),
472)
473@click.option(
474 "--include",
475 type=str,
476 default=DEFAULT_INCLUDES,
477 callback=validate_regex,
478 help=(
479 "A regular expression that matches files and directories that should be"
480 " included on recursive searches. An empty value means all files are included"
481 " regardless of the name. Use forward slashes for directories on all platforms"
482 " (Windows, too). Overrides all exclusions, including from .gitignore and"
483 " command line options."
484 ),
485 show_default=True,
486)
487@click.option(
488 "-W",
489 "--workers",
490 type=click.IntRange(min=1),
491 default=None,
492 help=(
493 "When Black formats multiple files, it may use a process pool to speed up"
494 " formatting. This option controls the number of parallel workers. This can"
495 " also be specified via the BLACK_NUM_WORKERS environment variable. Defaults"
496 " to the number of CPUs in the system."
497 ),
498)
499@click.option(
500 "-q",
501 "--quiet",
502 is_flag=True,
503 help=(
504 "Stop emitting all non-critical output. Error messages will still be emitted"
505 " (which can be silenced by 2>/dev/null)."
506 ),
507)
508@click.option(
509 "-v",
510 "--verbose",
511 is_flag=True,
512 help=(
513 "Emit messages about files that were not changed or were ignored due to"
514 " exclusion patterns. If Black is using a configuration file, a message"
515 " detailing which one it is using will be emitted."
516 ),
517)
518@click.version_option(
519 version=__version__,
520 message=(
521 f"%(prog)s, %(version)s (compiled: {'yes' if COMPILED else 'no'})\n"
522 f"Python ({platform.python_implementation()}) {platform.python_version()}"
523 ),
524)
525@click.argument(
526 "src",
527 nargs=-1,
528 type=click.Path(
529 exists=True, file_okay=True, dir_okay=True, readable=True, allow_dash=True
530 ),
531 is_eager=True,
532 metavar="SRC ...",
533)
534@click.option(
535 "--config",
536 type=click.Path(
537 exists=True,
538 file_okay=True,
539 dir_okay=False,
540 readable=True,
541 allow_dash=False,
542 path_type=str,
543 ),
544 is_eager=True,
545 callback=read_pyproject_toml,
546 help="Read configuration options from a configuration file.",
547)
548@click.option(
549 "--cache-dir",
550 type=click.Path(file_okay=False, path_type=Path),
551 help="Store the cache in this directory. Takes precedence over BLACK_CACHE_DIR.",
552)
553@click.option(
554 "--no-cache",
555 is_flag=True,
556 help=(
557 "Skip reading and writing the cache, forcing Black to reformat all"
558 " included files. Overrides --cache-dir."
559 ),
560)
561@click.pass_context
562def main(
563 ctx: click.Context,
564 code: str | None,
565 line_length: int,
566 target_version: list[TargetVersion],
567 check: bool,
568 diff: bool,
569 line_ranges: Sequence[str],
570 color: bool,
571 fast: bool,
572 pyi: bool,
573 ipynb: bool,
574 python_cell_magics: Sequence[str],
575 skip_source_first_line: bool,
576 skip_string_normalization: bool,
577 skip_magic_trailing_comma: bool,
578 preview: bool,
579 unstable: bool,
580 enable_unstable_feature: list[Preview],
581 quiet: bool,
582 verbose: bool,
583 required_version: str | None,
584 include: Pattern[str],
585 exclude: Pattern[str] | None,
586 extend_exclude: Pattern[str] | None,
587 force_exclude: Pattern[str] | None,
588 stdin_filename: str | None,
589 workers: int | None,
590 src: tuple[str, ...],
591 config: str | None,
592 cache_dir: Path | None,
593 no_cache: bool,
594) -> None:
595 """The uncompromising code formatter."""
596 ctx.ensure_object(dict)
598 assert sys.version_info >= (3, 10), "Black requires Python 3.10+"
599 if sys.version_info[:3] == (3, 12, 5):
600 out(
601 "Python 3.12.5 has a memory safety issue that can cause Black's "
602 "AST safety checks to fail. "
603 "Please upgrade to Python 3.12.6 or downgrade to Python 3.12.4"
604 )
605 ctx.exit(1)
607 if src and code is not None:
608 out(
609 main.get_usage(ctx)
610 + "\n\n'SRC' and 'code' cannot be passed simultaneously."
611 )
612 ctx.exit(1)
613 if not src and code is None:
614 out(main.get_usage(ctx) + "\n\nOne of 'SRC' or 'code' is required.")
615 ctx.exit(1)
617 # It doesn't do anything if --unstable is also passed, so just allow it.
618 if enable_unstable_feature and not (preview or unstable):
619 out(
620 main.get_usage(ctx)
621 + "\n\n'--enable-unstable-feature' requires '--preview'."
622 )
623 ctx.exit(1)
625 root, method = (
626 find_project_root(src, stdin_filename) if code is None else (None, None)
627 )
628 ctx.obj["root"] = root
630 if (
631 code is None
632 and root is None
633 and not quiet
634 and ctx.get_parameter_source("config") != ParameterSource.COMMANDLINE
635 ):
636 err(
637 "No project root could be identified: the given sources share no"
638 " common parent directory (for example, they are on different"
639 " drives). Black will use its default configuration. Pass"
640 " --config to select a configuration file or --quiet to silence"
641 " this warning."
642 )
644 if verbose:
645 if root:
646 out(
647 f"Identified `{root}` as project root containing a {method}.",
648 fg="blue",
649 )
651 if config:
652 config_source = ctx.get_parameter_source("config")
653 user_level_config = str(find_user_pyproject_toml())
654 if config == user_level_config:
655 out(
656 "Using configuration from user-level config at "
657 f"'{user_level_config}'.",
658 fg="blue",
659 )
660 elif config_source in (
661 ParameterSource.DEFAULT,
662 ParameterSource.DEFAULT_MAP,
663 ):
664 out("Using configuration from project root.", fg="blue")
665 else:
666 out(f"Using configuration in '{config}'.", fg="blue")
667 if ctx.default_map:
668 for param, value in ctx.default_map.items():
669 out(f"{param}: {value}")
671 error_msg = "Oh no! 💥 💔 💥"
672 if (
673 required_version
674 and required_version != __version__
675 and required_version != __version__.split(".")[0]
676 ):
677 err(
678 f"{error_msg} The required version `{required_version}` does not match"
679 f" the running version `{__version__}`!"
680 )
681 ctx.exit(1)
682 if ipynb and pyi:
683 err("Cannot pass both `pyi` and `ipynb` flags!")
684 ctx.exit(1)
686 write_back = WriteBack.from_configuration(check=check, diff=diff, color=color)
687 if cache_dir is None:
688 cache_dir = cache_module.CACHE_DIR
689 else:
690 if (
691 not cache_dir.is_absolute()
692 and ctx.get_parameter_source("cache_dir") == ParameterSource.DEFAULT_MAP
693 and config is not None
694 ):
695 cache_dir = Path(config).resolve().parent / cache_dir
696 cache_dir = get_cache_dir(cache_dir)
697 if not no_cache:
698 try:
699 cache_dir.mkdir(parents=True, exist_ok=True)
700 except OSError as e:
701 no_cache = True
702 if verbose:
703 out(
704 f"Unable to use cache directory `{cache_dir}`: {e}. "
705 "Disabling the cache.",
706 fg="blue",
707 )
708 if not no_cache:
709 pygram.initialize(cache_dir)
710 if target_version:
711 versions = set(target_version)
712 else:
713 # We'll autodetect later.
714 versions = set()
715 mode = Mode(
716 target_versions=versions,
717 line_length=line_length,
718 is_pyi=pyi,
719 is_ipynb=ipynb,
720 skip_source_first_line=skip_source_first_line,
721 string_normalization=not skip_string_normalization,
722 magic_trailing_comma=not skip_magic_trailing_comma,
723 preview=preview,
724 unstable=unstable,
725 python_cell_magics=set(python_cell_magics),
726 enabled_features=set(enable_unstable_feature),
727 )
729 if not fast and _target_versions_exceed_runtime(versions):
730 err(
731 f"Warning: {_version_mismatch_message(versions)} Black's safety"
732 " check verifies equivalence by parsing the AST, which fails"
733 " when the running Python is older than the target version.",
734 fg="yellow",
735 )
737 lines: list[tuple[int, int]] = []
738 if line_ranges:
739 if ipynb:
740 err("Cannot use --line-ranges with ipynb files.")
741 ctx.exit(1)
743 try:
744 lines = parse_line_ranges(line_ranges)
745 except ValueError as e:
746 err(str(e))
747 ctx.exit(1)
749 if code is not None:
750 # Run in quiet mode by default with -c; the extra output isn't useful.
751 # You can still pass -v to get verbose output.
752 quiet = True
754 report = Report(check=check, diff=diff, quiet=quiet, verbose=verbose)
756 if code is not None:
757 reformat_code(
758 content=code,
759 fast=fast,
760 write_back=write_back,
761 mode=mode,
762 report=report,
763 lines=lines,
764 )
765 else:
766 try:
767 sources = get_sources(
768 root=root,
769 src=src,
770 quiet=quiet,
771 verbose=verbose,
772 include=include,
773 exclude=exclude,
774 extend_exclude=extend_exclude,
775 force_exclude=force_exclude,
776 report=report,
777 stdin_filename=stdin_filename,
778 )
779 except GitIgnorePatternError:
780 ctx.exit(1)
782 if not sources:
783 if verbose or not quiet:
784 out("No Python files are present to be formatted. Nothing to do 😴")
785 if "-" in src and write_back is WriteBack.YES:
786 _echo_stdin_unchanged()
787 if "GITHUB_OUTPUT" in os.environ:
788 report.write_github_outputs(Path(os.environ["GITHUB_OUTPUT"]))
789 ctx.exit(0)
791 if len(sources) == 1:
792 reformat_one(
793 src=sources.pop(),
794 fast=fast,
795 write_back=write_back,
796 mode=mode,
797 report=report,
798 lines=lines,
799 no_cache=no_cache,
800 cache_dir=cache_dir,
801 )
802 else:
803 from black.concurrency import reformat_many
805 if lines:
806 err("Cannot use --line-ranges to format multiple files.")
807 ctx.exit(1)
808 reformat_many(
809 sources=sources,
810 fast=fast,
811 write_back=write_back,
812 mode=mode,
813 report=report,
814 workers=workers,
815 no_cache=no_cache,
816 cache_dir=cache_dir,
817 )
819 if verbose or not quiet:
820 if code is None and (verbose or report.change_count or report.failure_count):
821 out()
822 out(error_msg if report.return_code else "All done! ✨ 🍰 ✨")
823 if code is None:
824 click.echo(str(report), err=True)
825 if "GITHUB_OUTPUT" in os.environ:
826 report.write_github_outputs(Path(os.environ["GITHUB_OUTPUT"]))
827 ctx.exit(report.return_code)
830def get_sources(
831 *,
832 root: Path | None,
833 src: tuple[str, ...],
834 quiet: bool,
835 verbose: bool,
836 include: Pattern[str],
837 exclude: Pattern[str] | None,
838 extend_exclude: Pattern[str] | None,
839 force_exclude: Pattern[str] | None,
840 report: "Report",
841 stdin_filename: str | None,
842) -> set[Path]:
843 """Compute the set of files to be formatted."""
844 sources: set[Path] = set()
846 assert (
847 root is None or root.is_absolute()
848 ), f"INTERNAL ERROR: `root` must be absolute but is {root}"
849 using_default_exclude = exclude is None
850 exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES) if exclude is None else exclude
851 gitignore: dict[Path, GitIgnoreSpec] | None = None
852 root_gitignore = get_gitignore(root)
854 for s in src:
855 if s == "-" and stdin_filename:
856 path = Path(stdin_filename)
857 if path_is_excluded(stdin_filename, force_exclude):
858 report.path_ignored(
859 path,
860 "--stdin-filename matches the --force-exclude regular expression",
861 )
862 continue
863 is_stdin = True
864 else:
865 path = Path(s)
866 is_stdin = False
868 # When the sources share no common parent, there is no project root.
869 # Fall back to a per-source root so that exclusion, symlink, and
870 # gitignore handling still work for each source independently.
871 if root is None:
872 src_root = path.resolve() if path.is_dir() else path.resolve().parent
873 else:
874 src_root = root
876 # Compare the logic here to the logic in `gen_python_files`.
877 if is_stdin or path.is_file():
878 if resolves_outside_root_or_cannot_stat(path, src_root, report):
879 if verbose:
880 out(f'Skipping invalid source: "{path}"', fg="red")
881 continue
883 root_relative_path = best_effort_relative_path(path, src_root).as_posix()
884 root_relative_path = "/" + root_relative_path
886 # Hard-exclude any files that matches the `--force-exclude` regex.
887 if path_is_excluded(root_relative_path, force_exclude):
888 report.path_ignored(
889 path, "matches the --force-exclude regular expression"
890 )
891 continue
893 if is_stdin:
894 path = Path(f"{STDIN_PLACEHOLDER}{path}")
896 if path.suffix == ".ipynb" and not jupyter_dependencies_are_installed(
897 warn=verbose or not quiet
898 ):
899 continue
901 if verbose:
902 out(f'Found input source: "{path}"', fg="blue")
903 sources.add(path)
904 elif path.is_dir():
905 path = src_root / (path.resolve().relative_to(src_root))
906 if verbose:
907 out(f'Found input source directory: "{path}"', fg="blue")
909 if using_default_exclude:
910 gitignore = {
911 src_root: root_gitignore,
912 path: get_gitignore(path),
913 }
914 sources.update(
915 gen_python_files(
916 path.iterdir(),
917 src_root,
918 include,
919 exclude,
920 extend_exclude,
921 force_exclude,
922 report,
923 gitignore,
924 verbose=verbose,
925 quiet=quiet,
926 )
927 )
928 elif s == "-":
929 if verbose:
930 out("Found input source stdin", fg="blue")
931 sources.add(path)
932 else:
933 err(f"invalid path: {s}")
935 return sources
938def reformat_code(
939 content: str,
940 fast: bool,
941 write_back: WriteBack,
942 mode: Mode,
943 report: Report,
944 *,
945 lines: Collection[tuple[int, int]] = (),
946) -> None:
947 """
948 Reformat and print out `content` without spawning child processes.
949 Similar to `reformat_one`, but for string content.
951 `fast`, `write_back`, and `mode` options are passed to
952 :func:`format_file_in_place` or :func:`format_stdin_to_stdout`.
953 """
954 path = Path("<string>")
955 try:
956 changed = Changed.NO
957 if format_stdin_to_stdout(
958 content=content, fast=fast, write_back=write_back, mode=mode, lines=lines
959 ):
960 changed = Changed.YES
961 report.done(path, changed)
962 except Exception as exc:
963 if report.verbose:
964 traceback.print_exc()
965 report.failed(path, exc)
968# diff-shades depends on being to monkeypatch this function to operate. I know it's
969# not ideal, but this shouldn't cause any issues ... hopefully. ~ichard26
970@mypyc_attr(patchable=True)
971def reformat_one(
972 src: Path,
973 fast: bool,
974 write_back: WriteBack,
975 mode: Mode,
976 report: "Report",
977 *,
978 lines: Collection[tuple[int, int]] = (),
979 no_cache: bool = False,
980 cache_dir: Path | None = None,
981) -> None:
982 """Reformat a single file under `src` without spawning child processes.
984 `fast`, `write_back`, and `mode` options are passed to
985 :func:`format_file_in_place` or :func:`format_stdin_to_stdout`.
986 """
987 try:
988 changed = Changed.NO
990 if str(src) == "-":
991 is_stdin = True
992 elif str(src).startswith(STDIN_PLACEHOLDER):
993 is_stdin = True
994 # Use the original name again in case we want to print something
995 # to the user
996 src = Path(str(src)[len(STDIN_PLACEHOLDER) :])
997 else:
998 is_stdin = False
1000 if is_stdin:
1001 if src.suffix == ".pyi":
1002 mode = replace(mode, is_pyi=True)
1003 elif src.suffix == ".ipynb":
1004 mode = replace(mode, is_ipynb=True)
1005 if format_stdin_to_stdout(
1006 fast=fast, write_back=write_back, mode=mode, lines=lines
1007 ):
1008 changed = Changed.YES
1009 else:
1010 if no_cache:
1011 cache = None
1012 elif cache_dir is None:
1013 cache = Cache.read(mode)
1014 else:
1015 cache = Cache.read(mode, cache_dir)
1016 if cache is not None:
1017 if not cache.is_changed(src):
1018 changed = Changed.CACHED
1019 if changed is not Changed.CACHED and format_file_in_place(
1020 src, fast=fast, write_back=write_back, mode=mode, lines=lines
1021 ):
1022 changed = Changed.YES
1023 can_cache_unmodified = (
1024 write_back in (WriteBack.CHECK, WriteBack.DIFF, WriteBack.COLOR_DIFF)
1025 and changed is Changed.NO
1026 )
1027 # Formatting only some lines doesn't make the whole file formatted, and
1028 # the cache key doesn't include the line ranges, so don't record it.
1029 if (
1030 cache is not None
1031 and not lines
1032 and (
1033 (write_back is WriteBack.YES and changed is not Changed.CACHED)
1034 or can_cache_unmodified
1035 )
1036 ):
1037 cache.write([src])
1038 report.done(src, changed)
1039 except Exception as exc:
1040 if report.verbose:
1041 traceback.print_exc()
1042 report.failed(src, exc)
1045def format_file_in_place(
1046 src: Path,
1047 fast: bool,
1048 mode: Mode,
1049 write_back: WriteBack = WriteBack.NO,
1050 lock: Any = None, # multiprocessing.Manager().Lock() is some crazy proxy
1051 *,
1052 lines: Collection[tuple[int, int]] = (),
1053) -> bool:
1054 """Format file under `src` path. Return True if changed.
1056 If `write_back` is DIFF, write a diff to stdout. If it is YES, write reformatted
1057 code to the file.
1058 `mode` and `fast` options are passed to :func:`format_file_contents`.
1059 """
1060 if src.suffix == ".pyi":
1061 mode = replace(mode, is_pyi=True)
1062 elif src.suffix == ".ipynb":
1063 mode = replace(mode, is_ipynb=True)
1065 then = datetime.fromtimestamp(src.stat().st_mtime, timezone.utc)
1066 header = b""
1067 with open(src, "rb") as buf:
1068 if mode.skip_source_first_line:
1069 header = buf.readline()
1070 src_contents, encoding, newline = decode_bytes(buf.read(), mode)
1071 try:
1072 dst_contents = format_file_contents(
1073 src_contents, fast=fast, mode=mode, lines=lines
1074 )
1075 except NothingChanged:
1076 return False
1077 except JSONDecodeError:
1078 raise ValueError(
1079 f"File '{src}' cannot be parsed as valid Jupyter notebook."
1080 ) from None
1081 header_str = header.decode(encoding)
1082 if newline != "\n" and header_str.endswith(newline):
1083 # The contents are written with `newline` translation, which would turn the
1084 # header's original line ending into e.g. "\r\r\n".
1085 header_str = header_str[: -len(newline)] + "\n"
1086 src_contents = header_str + src_contents
1087 dst_contents = header_str + dst_contents
1089 if write_back == WriteBack.YES:
1090 with open(src, "w", encoding=encoding, newline=newline) as f:
1091 f.write(dst_contents)
1092 elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
1093 now = datetime.now(timezone.utc)
1094 src_name = f"{src}\t{then}"
1095 dst_name = f"{src}\t{now}"
1096 if mode.is_ipynb:
1097 diff_contents = ipynb_diff(src_contents, dst_contents, src_name, dst_name)
1098 else:
1099 diff_contents = diff(src_contents, dst_contents, src_name, dst_name)
1101 if write_back == WriteBack.COLOR_DIFF:
1102 diff_contents = color_diff(diff_contents)
1104 with lock or nullcontext():
1105 _write_to_stdout(
1106 diff_contents,
1107 encoding=encoding,
1108 newline=newline,
1109 wrap_for_windows=True,
1110 )
1112 return True
1115def format_stdin_to_stdout(
1116 fast: bool,
1117 *,
1118 content: str | None = None,
1119 write_back: WriteBack = WriteBack.NO,
1120 mode: Mode,
1121 lines: Collection[tuple[int, int]] = (),
1122) -> bool:
1123 """Format file on stdin. Return True if changed.
1125 If content is None, it's read from sys.stdin.
1127 If `write_back` is YES, write reformatted code back to stdout. If it is DIFF,
1128 write a diff to stdout. The `mode` argument is passed to
1129 :func:`format_file_contents`.
1130 """
1131 then = datetime.now(timezone.utc)
1133 header_str = ""
1134 if content is None:
1135 buf = io.BytesIO(sys.stdin.buffer.read())
1136 header = buf.readline() if mode.skip_source_first_line else b""
1137 src, encoding, newline = decode_bytes(buf.read(), mode)
1138 header_str = header.decode(encoding)
1139 if newline != "\n" and header_str.endswith(newline):
1140 # The output is written with `newline` translation, which would turn
1141 # the header's original line ending into e.g. "\r\r\n".
1142 header_str = header_str[: -len(newline)] + "\n"
1143 else:
1144 src, encoding, newline = content, "utf-8", "\n"
1146 dst = src
1147 try:
1148 dst = format_file_contents(src, fast=fast, mode=mode, lines=lines)
1149 return True
1151 except NothingChanged:
1152 return False
1154 finally:
1155 src = header_str + src
1156 dst = header_str + dst
1157 if write_back == WriteBack.YES:
1158 # Make sure there's a newline after the content. `_write_to_stdout()`
1159 # translates "\n" into `newline` itself, so append a bare "\n" here;
1160 # appending `newline` would write e.g. "\r\r\n" for CRLF input.
1161 if dst and dst[-1] != "\n" and dst[-1] != "\r":
1162 dst += "\n"
1163 _write_to_stdout(dst, encoding=encoding, newline=newline)
1164 elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
1165 now = datetime.now(timezone.utc)
1166 src_name = f"STDIN\t{then}"
1167 dst_name = f"STDOUT\t{now}"
1168 d = diff(src, dst, src_name, dst_name)
1169 if write_back == WriteBack.COLOR_DIFF:
1170 d = color_diff(d)
1171 _write_to_stdout(
1172 d,
1173 encoding=encoding,
1174 newline=newline,
1175 wrap_for_windows=write_back == WriteBack.COLOR_DIFF,
1176 )
1179def _write_to_stdout(
1180 content: str, *, encoding: str, newline: str, wrap_for_windows: bool = False
1181) -> None:
1182 """Write `content` to stdout with the given encoding and newline translation.
1184 `sys.stdout` isn't required to expose the underlying binary `buffer` (for
1185 example ipykernel's `OutStream` in Jupyter doesn't). In that case the stream
1186 only accepts text, so the newline translation that `io.TextIOWrapper` would do
1187 is applied here and the encoding is left to the stream itself.
1188 """
1189 buffer = getattr(sys.stdout, "buffer", None)
1190 if buffer is None:
1191 if newline != "\n":
1192 content = content.replace("\n", newline)
1193 stream = wrap_stream_for_windows(sys.stdout) if wrap_for_windows else sys.stdout
1194 stream.write(content)
1195 return
1197 f = io.TextIOWrapper(buffer, encoding=encoding, newline=newline, write_through=True)
1198 wrapped = wrap_stream_for_windows(f) if wrap_for_windows else f
1199 wrapped.write(content)
1200 f.detach()
1203def _echo_stdin_unchanged() -> None:
1204 """Copy stdin to stdout byte for byte.
1206 Going through the text streams instead would fail on input that isn't in their
1207 encoding (e.g. a latin-1 file) and, on Windows, turn LF line endings into CRLF.
1208 """
1209 in_buffer = getattr(sys.stdin, "buffer", None)
1210 out_buffer = getattr(sys.stdout, "buffer", None)
1211 if in_buffer is None or out_buffer is None:
1212 sys.stdout.write(sys.stdin.read())
1213 return
1215 out_buffer.write(in_buffer.read())
1216 out_buffer.flush()
1219def check_stability_and_equivalence(
1220 src_contents: str,
1221 dst_contents: str,
1222 *,
1223 mode: Mode,
1224 lines: Collection[tuple[int, int]] = (),
1225) -> None:
1226 """Perform stability and equivalence checks.
1228 Raise AssertionError if source and destination contents are not
1229 equivalent, or if a second pass of the formatter would format the
1230 content differently.
1231 """
1232 try:
1233 assert_equivalent(src_contents, dst_contents)
1234 except SourceASTParseError:
1235 raise
1236 except ASTSafetyError:
1237 if _target_versions_exceed_runtime(mode.target_versions):
1238 raise ASTSafetyError(
1239 "failed to verify equivalence of the formatted output:"
1240 f" {_version_mismatch_message(mode.target_versions)}"
1241 ) from None
1242 raise
1243 assert_stable(src_contents, dst_contents, mode=mode, lines=lines)
1246def format_file_contents(
1247 src_contents: str,
1248 *,
1249 fast: bool,
1250 mode: Mode,
1251 lines: Collection[tuple[int, int]] = (),
1252) -> FileContent:
1253 """Reformat contents of a file and return new contents.
1255 If `fast` is False, additionally confirm that the reformatted code is
1256 valid by calling :func:`assert_equivalent` and :func:`assert_stable` on it.
1257 `mode` is passed to :func:`format_str`.
1258 """
1259 if mode.is_ipynb:
1260 dst_contents = format_ipynb_string(src_contents, fast=fast, mode=mode)
1261 else:
1262 dst_contents = format_str(src_contents, mode=mode, lines=lines)
1263 if src_contents == dst_contents:
1264 raise NothingChanged
1266 if not fast and not mode.is_ipynb:
1267 # Jupyter notebooks will already have been checked above.
1268 check_stability_and_equivalence(
1269 src_contents, dst_contents, mode=mode, lines=lines
1270 )
1271 return dst_contents
1274def format_cell(src: str, *, fast: bool, mode: Mode) -> str:
1275 """Format code in given cell of Jupyter notebook.
1277 General idea is:
1279 - if cell has trailing semicolon, remove it;
1280 - if cell has IPython magics, mask them;
1281 - format cell;
1282 - reinstate IPython magics;
1283 - reinstate trailing semicolon (if originally present);
1284 - strip trailing newlines.
1286 Cells with syntax errors will not be processed, as they
1287 could potentially be automagics or multi-line magics, which
1288 are currently not supported.
1289 """
1290 validate_cell(src, mode)
1291 src_without_trailing_semicolon, has_trailing_semicolon = remove_trailing_semicolon(
1292 src
1293 )
1294 try:
1295 masked_src, replacements = mask_cell(src_without_trailing_semicolon)
1296 except SyntaxError:
1297 raise NothingChanged from None
1298 masked_dst = format_str(masked_src, mode=mode)
1299 if not fast:
1300 check_stability_and_equivalence(masked_src, masked_dst, mode=mode)
1301 dst_without_trailing_semicolon = unmask_cell(masked_dst, replacements)
1302 dst = put_trailing_semicolon_back(
1303 dst_without_trailing_semicolon, has_trailing_semicolon
1304 )
1305 dst = dst.rstrip("\n")
1306 if dst == src:
1307 raise NothingChanged from None
1308 return dst
1311def validate_metadata(nb: MutableMapping[str, Any]) -> None:
1312 """If notebook is marked as non-Python, don't format it.
1314 All notebook metadata fields are optional, see
1315 https://nbformat.readthedocs.io/en/stable/format_description.html. So
1316 if a notebook has empty metadata, we will try to parse it anyway.
1317 """
1318 language = nb.get("metadata", {}).get("language_info", {}).get("name", None)
1319 if language is not None and language != "python":
1320 raise NothingChanged from None
1323def format_ipynb_string(src_contents: str, *, fast: bool, mode: Mode) -> FileContent:
1324 """Format Jupyter notebook.
1326 Operate cell-by-cell, only on code cells, only for Python notebooks.
1327 If the ``.ipynb`` originally had a trailing newline, it'll be preserved.
1328 """
1329 if not src_contents:
1330 raise NothingChanged
1332 trailing_newline = src_contents[-1] == "\n"
1333 modified = False
1334 nb = json.loads(src_contents)
1335 validate_metadata(nb)
1336 for cell in nb["cells"]:
1337 if cell.get("cell_type", None) == "code":
1338 try:
1339 src = "".join(cell["source"])
1340 dst = format_cell(src, fast=fast, mode=mode)
1341 except NothingChanged:
1342 pass
1343 else:
1344 cell["source"] = dst.splitlines(keepends=True)
1345 modified = True
1346 if modified:
1347 dst_contents = json.dumps(nb, indent=1, ensure_ascii=False)
1348 if trailing_newline:
1349 dst_contents = dst_contents + "\n"
1350 return dst_contents
1351 else:
1352 raise NothingChanged
1355def format_str(
1356 src_contents: str, *, mode: Mode, lines: Collection[tuple[int, int]] = ()
1357) -> str:
1358 """Reformat a string and return new contents.
1360 `mode` determines formatting options, such as how many characters per line are
1361 allowed. Example:
1363 >>> import black
1364 >>> print(black.format_str("def f(arg:str='')->None:...", mode=black.Mode()))
1365 def f(arg: str = "") -> None:
1366 ...
1368 A more complex example:
1370 >>> print(
1371 ... black.format_str(
1372 ... "def f(arg:str='')->None: hey",
1373 ... mode=black.Mode(
1374 ... target_versions={black.TargetVersion.PY36},
1375 ... line_length=10,
1376 ... string_normalization=False,
1377 ... is_pyi=False,
1378 ... ),
1379 ... ),
1380 ... )
1381 def f(
1382 arg: str = '',
1383 ) -> None:
1384 hey
1386 """
1387 if lines:
1388 lines = sanitized_lines(lines, src_contents)
1389 if not lines:
1390 return src_contents # Nothing to format
1391 dst_contents = _format_str_once(src_contents, mode=mode, lines=lines)
1392 # Forced second pass to work around optional trailing commas (becoming
1393 # forced trailing commas on pass 2) interacting differently with optional
1394 # parentheses. Admittedly ugly.
1395 if src_contents != dst_contents:
1396 if lines:
1397 lines = adjusted_lines(lines, src_contents, dst_contents)
1398 if not lines:
1399 # None of the requested lines survived the first pass, so there is
1400 # nothing left to format. Passing the empty `lines` to the second
1401 # pass would format the whole file instead.
1402 return dst_contents
1403 try:
1404 dst_contents = _format_str_once(dst_contents, mode=mode, lines=lines)
1405 except InvalidInput as exc:
1406 # This pass parses Black's own output, so failing to parse it means
1407 # Black produced the invalid code, not the user. Reported as an
1408 # internal error rather than as a syntax error in the user's file,
1409 # keeping the parse location, which points into Black's output.
1410 log = dump_to_file(dst_contents)
1411 exc.context = (
1412 f"INTERNAL ERROR: {_black_info()} produced invalid code:\n"
1413 f"{exc.context or 'cannot parse'}"
1414 )
1415 exc.details = (
1416 f"{exc.details or ''}\n"
1417 "Please report a bug on https://github.com/psf/black/issues. "
1418 f"This invalid output might be helpful: {log}"
1419 )
1420 exc.args = (
1421 f"{exc.context}: {exc.lineno}:{exc.column}{exc.details}",
1422 exc.lineno,
1423 exc.column,
1424 exc.context,
1425 exc.details,
1426 )
1427 raise
1428 if lines:
1429 dst_contents = _restore_unselected_trailing_blank_lines(
1430 src_contents, dst_contents, lines
1431 )
1432 return dst_contents
1435def _restore_unselected_trailing_blank_lines(
1436 src_contents: str,
1437 dst_contents: str,
1438 lines: Collection[tuple[int, int]],
1439) -> str:
1440 """Restore trailing whitespace-only lines outside requested line ranges."""
1441 src_lines = src_contents.splitlines(keepends=True)
1442 dst_lines = dst_contents.splitlines(keepends=True)
1443 if len(dst_lines) >= len(src_lines):
1444 return dst_contents
1446 suffix = src_lines[len(dst_lines) :]
1447 first_suffix_line = len(dst_lines) + 1
1448 if all(
1449 not line.strip()
1450 and not any(start <= line_number <= end for start, end in lines)
1451 for line_number, line in enumerate(suffix, start=first_suffix_line)
1452 ):
1453 return dst_contents + "".join(suffix)
1454 return dst_contents
1457def _format_str_once(
1458 src_contents: str, *, mode: Mode, lines: Collection[tuple[int, int]] = ()
1459) -> str:
1460 # Use the encoding overwrite since the src_contents may contain a different
1461 # magic encoding comment than utf-8
1462 normalized_contents, _, newline_type = decode_bytes(
1463 src_contents.encode("utf-8"), mode, encoding_overwrite="utf-8"
1464 )
1466 src_node = lib2to3_parse(
1467 normalized_contents.lstrip(), target_versions=mode.target_versions
1468 )
1470 dst_blocks: list[LinesBlock] = []
1471 if mode.target_versions:
1472 versions = mode.target_versions
1473 else:
1474 future_imports = get_future_imports(src_node)
1475 versions = detect_target_versions(src_node, future_imports=future_imports)
1477 line_generation_features = {
1478 feature
1479 for feature in {
1480 Feature.PARENTHESIZED_CONTEXT_MANAGERS,
1481 Feature.UNPARENTHESIZED_EXCEPT_TYPES,
1482 Feature.T_STRINGS,
1483 Feature.UNPARENTHESIZED_WALRUS_IN_SETS,
1484 }
1485 if supports_feature(versions, feature)
1486 }
1487 normalize_fmt_off(src_node, mode, lines)
1488 if lines:
1489 # This should be called after normalize_fmt_off.
1490 convert_unchanged_lines(src_node, lines)
1492 line_generator = LineGenerator(mode=mode, features=line_generation_features)
1493 elt = EmptyLineTracker(mode=mode)
1494 split_line_features = {
1495 feature
1496 for feature in {
1497 Feature.TRAILING_COMMA_IN_CALL,
1498 Feature.TRAILING_COMMA_IN_DEF,
1499 }
1500 if supports_feature(versions, feature)
1501 }
1502 block: LinesBlock | None = None
1503 for current_line in line_generator.visit(src_node):
1504 block = elt.maybe_empty_lines(current_line)
1505 dst_blocks.append(block)
1506 for line in transform_line(
1507 current_line, mode=mode, features=split_line_features
1508 ):
1509 block.content_lines.append(str(line))
1510 if dst_blocks:
1511 dst_blocks[-1].after = 0
1512 dst_contents = []
1513 for block in dst_blocks:
1514 dst_contents.extend(block.all_lines())
1515 if not dst_contents:
1516 if "\n" in normalized_contents:
1517 return newline_type
1518 dst = "".join(dst_contents)
1519 if not mode.is_ipynb:
1520 dst = _keep_encoding_declaration_ignored(normalized_contents, dst)
1521 return dst.replace("\n", newline_type)
1524def _declared_encoding(contents: str) -> str | None:
1525 """Return the encoding Python would decode `contents` with.
1527 Only an encoding declaration on the first two lines counts (PEP 263). Returns
1528 None if the declared encoding is invalid.
1529 """
1530 first_two_lines = "\n".join(contents.split("\n", 2)[:2])
1531 try:
1532 encoding, _ = tokenize.detect_encoding(
1533 io.BytesIO(first_two_lines.encode("utf-8")).readline
1534 )
1535 except SyntaxError:
1536 return None
1537 return encoding
1540def _keep_encoding_declaration_ignored(src_contents: str, dst_contents: str) -> str:
1541 """Retain up to two blank lines so an ignored encoding declaration stays ignored.
1543 Python ignores an encoding declaration below the second line. Removing the
1544 blank lines at the start of the file can move one onto the first two lines,
1545 where it takes effect and changes how the file is decoded.
1546 """
1547 src_encoding = _declared_encoding(src_contents)
1548 for blank_lines in range(3):
1549 retained = "\n" * blank_lines + dst_contents
1550 if _declared_encoding(retained) == src_encoding:
1551 return retained
1552 return dst_contents
1555def decode_bytes(
1556 src: bytes, mode: Mode, *, encoding_overwrite: str | None = None
1557) -> tuple[FileContent, Encoding, NewLine]:
1558 """Return a tuple of (decoded_contents, encoding, newline).
1560 `newline` is either CRLF, LF, or CR, but `decoded_contents` is decoded with
1561 universal newlines (i.e. only contains LF).
1563 Use the keyword only encoding_overwrite argument if the bytes are encoded
1564 differently to their possible encoding magic comment.
1565 """
1566 srcbuf = io.BytesIO(src)
1568 # Still use detect encoding even if overwrite set because otherwise lines
1569 # might be different
1570 encoding, lines = tokenize.detect_encoding(srcbuf.readline)
1571 if encoding_overwrite is not None:
1572 encoding = encoding_overwrite
1574 if not lines:
1575 return "", encoding, "\n"
1577 if lines[0][-2:] == b"\r\n":
1578 if b"\r" in lines[0][:-2]:
1579 newline = "\r"
1580 else:
1581 newline = "\r\n"
1582 elif lines[0][-1:] == b"\n":
1583 if b"\r" in lines[0][:-1]:
1584 newline = "\r"
1585 else:
1586 newline = "\n"
1587 else:
1588 if b"\r" in lines[0]:
1589 newline = "\r"
1590 else:
1591 newline = "\n"
1593 srcbuf.seek(0)
1594 with io.TextIOWrapper(srcbuf, encoding) as tiow:
1595 return tiow.read(), encoding, newline
1598def get_features_used(
1599 node: Node, *, future_imports: set[str] | None = None
1600) -> set[Feature]:
1601 """Return a set of (relatively) new Python features used in this file.
1603 Currently looking for:
1604 - f-strings;
1605 - self-documenting expressions in f-strings (f"{x=}");
1606 - underscores in numeric literals;
1607 - trailing commas after * or ** in function signatures and calls;
1608 - positional only arguments in function signatures and lambdas;
1609 - assignment expression;
1610 - relaxed decorator syntax;
1611 - usage of __future__ flags (annotations);
1612 - print / exec statements;
1613 - parenthesized context managers;
1614 - match statements;
1615 - except* clause;
1616 - variadic generics;
1617 - lazy imports;
1618 - starred or double-starred comprehensions.
1619 """
1620 features: set[Feature] = set()
1621 if future_imports:
1622 features |= {
1623 FUTURE_FLAG_TO_FEATURE[future_import]
1624 for future_import in future_imports
1625 if future_import in FUTURE_FLAG_TO_FEATURE
1626 }
1628 for n in node.pre_order():
1629 if n.type == token.FSTRING_START:
1630 features.add(Feature.F_STRINGS)
1631 elif n.type == token.TSTRING_START:
1632 features.add(Feature.T_STRINGS)
1633 elif (
1634 n.type == token.RBRACE
1635 and n.parent is not None
1636 and any(child.type == token.EQUAL for child in n.parent.children)
1637 ):
1638 features.add(Feature.DEBUG_F_STRINGS)
1640 elif is_number_token(n):
1641 if "_" in n.value:
1642 features.add(Feature.NUMERIC_UNDERSCORES)
1644 elif n.type == token.SLASH:
1645 if n.parent and n.parent.type in {
1646 syms.typedargslist,
1647 syms.arglist,
1648 syms.varargslist,
1649 }:
1650 features.add(Feature.POS_ONLY_ARGUMENTS)
1652 elif n.type == token.COLONEQUAL:
1653 features.add(Feature.ASSIGNMENT_EXPRESSIONS)
1655 elif n.type == token.LAZY:
1656 features.add(Feature.LAZY_IMPORTS)
1658 elif n.type == syms.decorator:
1659 if len(n.children) > 1 and not is_simple_decorator_expression(
1660 n.children[1]
1661 ):
1662 features.add(Feature.RELAXED_DECORATORS)
1664 elif is_unpacking_comprehension(n):
1665 features.add(Feature.UNPACKING_IN_COMPREHENSIONS)
1667 elif (
1668 n.type in {syms.typedargslist, syms.arglist}
1669 and n.children
1670 and n.children[-1].type == token.COMMA
1671 ):
1672 if n.type == syms.typedargslist:
1673 feature = Feature.TRAILING_COMMA_IN_DEF
1674 else:
1675 feature = Feature.TRAILING_COMMA_IN_CALL
1677 for ch in n.children:
1678 if ch.type in STARS:
1679 features.add(feature)
1681 if ch.type == syms.argument:
1682 for argch in ch.children:
1683 if argch.type in STARS:
1684 features.add(feature)
1686 elif (
1687 n.type in {syms.return_stmt, syms.yield_expr}
1688 and len(n.children) >= 2
1689 and n.children[1].type == syms.testlist_star_expr
1690 and any(child.type == syms.star_expr for child in n.children[1].children)
1691 ):
1692 features.add(Feature.UNPACKING_ON_FLOW)
1694 elif (
1695 n.type == syms.annassign
1696 and len(n.children) >= 4
1697 and n.children[3].type == syms.testlist_star_expr
1698 ):
1699 features.add(Feature.ANN_ASSIGN_EXTENDED_RHS)
1701 elif (
1702 n.type == syms.with_stmt
1703 and len(n.children) > 2
1704 and n.children[1].type == syms.atom
1705 ):
1706 atom_children = n.children[1].children
1707 if (
1708 len(atom_children) == 3
1709 and atom_children[0].type == token.LPAR
1710 and _contains_asexpr(atom_children[1])
1711 and atom_children[2].type == token.RPAR
1712 ):
1713 features.add(Feature.PARENTHESIZED_CONTEXT_MANAGERS)
1715 elif n.type == syms.match_stmt:
1716 features.add(Feature.PATTERN_MATCHING)
1718 elif n.type in {syms.subscriptlist, syms.trailer} and any(
1719 child.type == syms.star_expr for child in n.children
1720 ):
1721 features.add(Feature.VARIADIC_GENERICS)
1723 elif (
1724 n.type == syms.tname_star
1725 and len(n.children) == 3
1726 and n.children[2].type == syms.star_expr
1727 ):
1728 features.add(Feature.VARIADIC_GENERICS)
1730 elif n.type in (syms.type_stmt, syms.typeparams):
1731 features.add(Feature.TYPE_PARAMS)
1733 elif (
1734 n.type in (syms.typevartuple, syms.paramspec, syms.typevar)
1735 and n.children[-2].type == token.EQUAL
1736 ):
1737 features.add(Feature.TYPE_PARAM_DEFAULTS)
1739 elif (
1740 n.type == syms.except_clause
1741 and len(n.children) >= 2
1742 and (
1743 n.children[1].type == token.STAR or n.children[1].type == syms.testlist
1744 )
1745 ):
1746 is_star_except = n.children[1].type == token.STAR
1748 if is_star_except:
1749 features.add(Feature.EXCEPT_STAR)
1751 # Presence of except* pushes as clause 1 index back
1752 has_as_clause = (
1753 len(n.children) >= is_star_except + 3
1754 and n.children[is_star_except + 2].type == token.NAME
1755 and n.children[is_star_except + 2].value == "as" # type: ignore
1756 )
1758 # If there's no 'as' clause and the except expression is a testlist.
1759 if not has_as_clause and (
1760 (is_star_except and n.children[2].type == syms.testlist)
1761 or (not is_star_except and n.children[1].type == syms.testlist)
1762 ):
1763 features.add(Feature.UNPARENTHESIZED_EXCEPT_TYPES)
1765 return features
1768def is_unpacking_comprehension(node: LN) -> bool:
1769 if node.type not in {
1770 syms.listmaker,
1771 syms.testlist_gexp,
1772 syms.dictsetmaker,
1773 syms.argument,
1774 }:
1775 return False
1777 if not any(
1778 child.type in {syms.comp_for, syms.old_comp_for} for child in node.children
1779 ):
1780 return False
1782 first_child = node.children[0]
1783 return first_child.type in {syms.star_expr, token.STAR, token.DOUBLESTAR}
1786def _contains_asexpr(node: Node | Leaf) -> bool:
1787 """Return True if `node` contains an as-pattern."""
1788 if node.type == syms.asexpr_test:
1789 return True
1790 elif node.type == syms.atom:
1791 if (
1792 len(node.children) == 3
1793 and node.children[0].type == token.LPAR
1794 and node.children[2].type == token.RPAR
1795 ):
1796 return _contains_asexpr(node.children[1])
1797 elif node.type == syms.testlist_gexp:
1798 return any(_contains_asexpr(child) for child in node.children)
1799 return False
1802def detect_target_versions(
1803 node: Node, *, future_imports: set[str] | None = None
1804) -> set[TargetVersion]:
1805 """Detect the version to target based on the nodes used."""
1806 features = get_features_used(node, future_imports=future_imports)
1807 return {
1808 version for version in TargetVersion if features <= VERSION_TO_FEATURES[version]
1809 }
1812def get_future_imports(node: Node) -> set[str]:
1813 """Return a set of __future__ imports in the file."""
1814 imports: set[str] = set()
1816 def get_imports_from_children(children: list[LN]) -> Generator[str, None, None]:
1817 for child in children:
1818 if isinstance(child, Leaf):
1819 if child.type == token.NAME:
1820 yield child.value
1822 elif child.type == syms.import_as_name:
1823 orig_name = child.children[0]
1824 assert isinstance(orig_name, Leaf), "Invalid syntax parsing imports"
1825 assert orig_name.type == token.NAME, "Invalid syntax parsing imports"
1826 yield orig_name.value
1828 elif child.type == syms.import_as_names:
1829 yield from get_imports_from_children(child.children)
1831 else:
1832 raise AssertionError("Invalid syntax parsing imports")
1834 for child in node.children:
1835 if child.type != syms.simple_stmt:
1836 break
1838 first_child = child.children[0]
1839 if isinstance(first_child, Leaf):
1840 # Continue looking if we see a docstring; otherwise stop.
1841 if (
1842 len(child.children) == 2
1843 and first_child.type == token.STRING
1844 and child.children[1].type == token.NEWLINE
1845 ):
1846 continue
1848 break
1850 elif first_child.type == syms.import_from:
1851 if first_child.children[0].type == token.LAZY:
1852 break
1854 module_name = first_child.children[1]
1855 if not isinstance(module_name, Leaf) or module_name.value != "__future__":
1856 break
1858 imports |= set(get_imports_from_children(first_child.children[3:]))
1859 else:
1860 break
1862 return imports
1865def _black_info() -> str:
1866 return (
1867 f"Black {__version__} on "
1868 f"Python ({platform.python_implementation()}) {platform.python_version()}"
1869 )
1872def assert_equivalent(src: str, dst: str) -> None:
1873 """Raise AssertionError if `src` and `dst` aren't equivalent."""
1874 try:
1875 src_ast = parse_ast(src)
1876 except Exception as exc:
1877 raise SourceASTParseError(
1878 "cannot use --safe with this file; failed to parse source file AST: "
1879 f"{exc}\n"
1880 "This could be caused by running Black with an older Python version "
1881 "that does not support new syntax used in your source file."
1882 ) from exc
1884 try:
1885 dst_ast = parse_ast(dst)
1886 except Exception as exc:
1887 log = dump_to_file("".join(traceback.format_tb(exc.__traceback__)), dst)
1888 raise ASTSafetyError(
1889 f"INTERNAL ERROR: {_black_info()} produced invalid code: {exc}. "
1890 "Please report a bug on https://github.com/psf/black/issues. "
1891 f"This invalid output might be helpful: {log}"
1892 ) from None
1894 src_ast_str = "\n".join(stringify_ast(src_ast))
1895 dst_ast_str = "\n".join(stringify_ast(dst_ast))
1896 if src_ast_str != dst_ast_str:
1897 log = dump_to_file(diff(src_ast_str, dst_ast_str, "src", "dst"))
1898 raise ASTSafetyError(
1899 f"INTERNAL ERROR: {_black_info()} produced code that is not equivalent to"
1900 " the source. Please report a bug on https://github.com/psf/black/issues."
1901 f" This diff might be helpful: {log}"
1902 ) from None
1905def assert_stable(
1906 src: str, dst: str, mode: Mode, *, lines: Collection[tuple[int, int]] = ()
1907) -> None:
1908 """Raise AssertionError if `dst` reformats differently the second time."""
1909 if lines:
1910 # Formatting specified lines requires `adjusted_lines` to map original lines
1911 # to the formatted lines before re-formatting the previously formatted result.
1912 # Due to less-ideal diff algorithm, some edge cases produce incorrect new line
1913 # ranges. Hence for now, we skip the stable check.
1914 # See https://github.com/psf/black/issues/4033 for context.
1915 return
1916 # We shouldn't call format_str() here, because that formats the string
1917 # twice and may hide a bug where we bounce back and forth between two
1918 # versions.
1919 newdst = _format_str_once(dst, mode=mode, lines=lines)
1920 if dst != newdst:
1921 log = dump_to_file(
1922 str(mode),
1923 diff(src, dst, "source", "first pass"),
1924 diff(dst, newdst, "first pass", "second pass"),
1925 )
1926 raise AssertionError(
1927 f"INTERNAL ERROR: {_black_info()} produced different code on the second"
1928 " pass of the formatter. Please report a bug on"
1929 f" https://github.com/psf/black/issues. This diff might be helpful: {log}"
1930 ) from None
1933def patched_main() -> None:
1934 # PyInstaller patches multiprocessing to need freeze_support() even in non-Windows
1935 # environments so just assume we always need to call it if frozen.
1936 if getattr(sys, "frozen", False):
1937 from multiprocessing import freeze_support
1939 freeze_support()
1941 main()
1944if __name__ == "__main__":
1945 patched_main()