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

659 statements  

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 

22 

23import click 

24from click.core import ParameterSource 

25from mypy_extensions import mypyc_attr 

26from pathspec import GitIgnoreSpec 

27from pathspec.patterns.gitignore import GitIgnorePatternError 

28 

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 

83 

84COMPILED = Path(__file__).suffix in (".pyd", ".so") 

85 

86# types 

87FileContent = str 

88Encoding = str 

89NewLine = str 

90 

91 

92class WriteBack(Enum): 

93 NO = 0 

94 YES = 1 

95 DIFF = 2 

96 CHECK = 3 

97 COLOR_DIFF = 4 

98 

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 

105 

106 if diff and color: 

107 return cls.COLOR_DIFF 

108 

109 return cls.DIFF if diff else cls.YES 

110 

111 

112# Legacy name, left for integrations. 

113FileMode = Mode 

114 

115 

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`. 

120 

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 

130 

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 

137 

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 } 

149 

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 ) 

155 

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") 

159 

160 include = config.get("include") 

161 if include is not None and not isinstance(include, str): 

162 raise click.BadOptionUsage("include", "Config key include must be a string") 

163 

164 extend_exclude = config.get("extend_exclude") 

165 if extend_exclude is not None and not isinstance(extend_exclude, str): 

166 raise click.BadOptionUsage( 

167 "extend-exclude", "Config key extend-exclude must be a string" 

168 ) 

169 

170 force_exclude = config.get("force_exclude") 

171 if force_exclude is not None and not isinstance(force_exclude, str): 

172 raise click.BadOptionUsage( 

173 "force-exclude", "Config key force-exclude must be a string" 

174 ) 

175 

176 line_ranges = config.get("line_ranges") 

177 if line_ranges is not None: 

178 raise click.BadOptionUsage( 

179 "line-ranges", "Cannot use line-ranges in the pyproject.toml file." 

180 ) 

181 

182 default_map: dict[str, Any] = {} 

183 if ctx.default_map: 

184 default_map.update(ctx.default_map) 

185 default_map.update(config) 

186 

187 ctx.default_map = default_map 

188 return value 

189 

190 

191def spellcheck_pyproject_toml_keys( 

192 ctx: click.Context, config_keys: list[str], config_file_path: str 

193) -> None: 

194 invalid_keys: list[str] = [] 

195 available_config_options = {param.name for param in ctx.command.params} 

196 invalid_keys = [key for key in config_keys if key not in available_config_options] 

197 if invalid_keys: 

198 keys_str = ", ".join(map(repr, invalid_keys)) 

199 out( 

200 f"Invalid config keys detected: {keys_str} (in {config_file_path})", 

201 fg="red", 

202 ) 

203 

204 

205def target_version_option_callback( 

206 c: click.Context, p: click.Option | click.Parameter, v: tuple[str, ...] 

207) -> list[TargetVersion]: 

208 """Compute the target versions from a --target-version flag. 

209 

210 This is its own function because mypy couldn't infer the type correctly 

211 when it was a lambda, causing mypyc trouble. 

212 """ 

213 return [TargetVersion[val.upper()] for val in v] 

214 

215 

216def _target_versions_exceed_runtime( 

217 target_versions: set[TargetVersion], 

218) -> bool: 

219 """Check if ALL target versions exceed the runtime Python version. 

220 

221 If any target version is at or below the runtime version, the AST 

222 safety check can succeed for that version's features, so the warning 

223 would be spurious. 

224 """ 

225 if not target_versions: 

226 return False 

227 min_target_minor = min(tv.value for tv in target_versions) 

228 return min_target_minor > sys.version_info[1] 

229 

230 

231def _version_mismatch_message(target_versions: set[TargetVersion]) -> str: 

232 max_target = max(target_versions, key=lambda tv: tv.value) 

233 runtime = f"{sys.version_info[0]}.{sys.version_info[1]}" 

234 return ( 

235 f"Python {runtime} cannot parse code formatted for" 

236 f" {max_target.pretty()}. To fix this: run Black with" 

237 f" {max_target.pretty()}, set --target-version to" 

238 f" py3{sys.version_info[1]}, or use --fast to skip the safety" 

239 " check." 

240 ) 

241 

242 

243def enable_unstable_feature_callback( 

244 c: click.Context, p: click.Option | click.Parameter, v: tuple[str, ...] 

245) -> list[Preview]: 

246 """Compute the features from an --enable-unstable-feature flag.""" 

247 return [Preview[val] for val in v] 

248 

249 

250def re_compile_maybe_verbose(regex: str) -> Pattern[str]: 

251 """Compile a regular expression string in `regex`. 

252 

253 If it contains newlines, use verbose mode. 

254 """ 

255 if "\n" in regex: 

256 regex = "(?x)" + regex 

257 compiled: Pattern[str] = re.compile(regex) 

258 return compiled 

259 

260 

261def validate_regex( 

262 ctx: click.Context, 

263 param: click.Parameter, 

264 value: str | None, 

265) -> Pattern[str] | None: 

266 try: 

267 return re_compile_maybe_verbose(value) if value is not None else None 

268 except re.error as e: 

269 raise click.BadParameter(f"Not a valid regular expression: {e}") from None 

270 

271 

272@click.command( 

273 context_settings={"help_option_names": ["-h", "--help"]}, 

274 # While Click does set this field automatically using the docstring, mypyc 

275 # (annoyingly) strips 'em so we need to set it here too. 

276 help="The uncompromising code formatter.", 

277) 

278@click.option("-c", "--code", type=str, help="Format the code passed in as a string.") 

279@click.option( 

280 "-l", 

281 "--line-length", 

282 type=int, 

283 default=DEFAULT_LINE_LENGTH, 

284 help="How many characters per line to allow.", 

285 show_default=True, 

286) 

287@click.option( 

288 "-t", 

289 "--target-version", 

290 type=click.Choice([v.name.lower() for v in TargetVersion]), 

291 callback=target_version_option_callback, 

292 multiple=True, 

293 help=( 

294 "Python versions that should be supported by Black's output. You should" 

295 " include all versions that your code supports. By default, Black will infer" 

296 " target versions from the project metadata in pyproject.toml. If this does" 

297 " not yield conclusive results, Black will use per-file auto-detection." 

298 ), 

299) 

300@click.option( 

301 "--pyi", 

302 is_flag=True, 

303 help=( 

304 "Format all input files like typing stubs regardless of file extension. This" 

305 " is useful when piping source on standard input." 

306 ), 

307) 

308@click.option( 

309 "--ipynb", 

310 is_flag=True, 

311 help=( 

312 "Format all input files like Jupyter Notebooks regardless of file extension." 

313 " This is useful when piping source on standard input." 

314 ), 

315) 

316@click.option( 

317 "--python-cell-magics", 

318 multiple=True, 

319 help=( 

320 "When processing Jupyter Notebooks, add the given magic to the list" 

321 f" of known python-magics ({', '.join(sorted(PYTHON_CELL_MAGICS))})." 

322 " Useful for formatting cells with custom python magics." 

323 ), 

324 default=[], 

325) 

326@click.option( 

327 "-x", 

328 "--skip-source-first-line", 

329 is_flag=True, 

330 help="Skip the first line of the source code.", 

331) 

332@click.option( 

333 "-S", 

334 "--skip-string-normalization", 

335 is_flag=True, 

336 help="Don't normalize string quotes or prefixes.", 

337) 

338@click.option( 

339 "-C", 

340 "--skip-magic-trailing-comma", 

341 is_flag=True, 

342 help="Don't use trailing commas as a reason to split lines.", 

343) 

344@click.option( 

345 "--preview", 

346 is_flag=True, 

347 help=( 

348 "Enable potentially disruptive style changes that may be added to Black's main" 

349 " functionality in the next major release." 

350 ), 

351) 

352@click.option( 

353 "--unstable", 

354 is_flag=True, 

355 help=( 

356 "Enable potentially disruptive style changes that have known bugs or are not" 

357 " currently expected to make it into the stable style Black's next major" 

358 " release. Implies --preview." 

359 ), 

360) 

361@click.option( 

362 "--enable-unstable-feature", 

363 type=click.Choice([v.name for v in Preview]), 

364 callback=enable_unstable_feature_callback, 

365 multiple=True, 

366 help=( 

367 "Enable specific features included in the `--unstable` style. Requires" 

368 " `--preview`. No compatibility guarantees are provided on the behavior" 

369 " or existence of any unstable features." 

370 ), 

371) 

372@click.option( 

373 "--check", 

374 is_flag=True, 

375 help=( 

376 "Don't write the files back, just return the status. Return code 0 means" 

377 " nothing would change. Return code 1 means some files would be reformatted." 

378 " Return code 123 means there was an internal error." 

379 ), 

380) 

381@click.option( 

382 "--diff", 

383 is_flag=True, 

384 help=( 

385 "Don't write the files back, just output a diff to indicate what changes" 

386 " Black would've made. They are printed to stdout so capturing them is simple." 

387 ), 

388) 

389@click.option( 

390 "--color/--no-color", 

391 is_flag=True, 

392 help="Show (or do not show) colored diff. Only applies when --diff is given.", 

393) 

394@click.option( 

395 "--line-ranges", 

396 multiple=True, 

397 metavar="START-END", 

398 help=( 

399 "When specified, Black will try its best to only format these lines. This" 

400 " option can be specified multiple times, and a union of the lines will be" 

401 " formatted. Each range must be specified as two integers connected by a `-`:" 

402 " `<START>-<END>`. The `<START>` and `<END>` integer indices are 1-based and" 

403 " inclusive on both ends." 

404 ), 

405 default=(), 

406) 

407@click.option( 

408 "--fast/--safe", 

409 is_flag=True, 

410 help=( 

411 "By default, Black performs an AST safety check after formatting your code." 

412 " The --fast flag turns off this check and the --safe flag explicitly enables" 

413 " it. [default: --safe]" 

414 ), 

415) 

416@click.option( 

417 "--required-version", 

418 type=str, 

419 help=( 

420 "Require a specific version of Black to be running. This is useful for" 

421 " ensuring that all contributors to your project are using the same" 

422 " version, because different versions of Black may format code a little" 

423 " differently. This option can be set in a configuration file for consistent" 

424 " results across environments." 

425 ), 

426) 

427@click.option( 

428 "--exclude", 

429 type=str, 

430 callback=validate_regex, 

431 help=( 

432 "A regular expression that matches files and directories that should be" 

433 " excluded on recursive searches. An empty value means no paths are excluded." 

434 " Use forward slashes for directories on all platforms (Windows, too)." 

435 " By default, Black also ignores all paths listed in .gitignore. Changing this" 

436 f" value will override all default exclusions. [default: {DEFAULT_EXCLUDES}]" 

437 ), 

438 show_default=False, 

439) 

440@click.option( 

441 "--extend-exclude", 

442 type=str, 

443 callback=validate_regex, 

444 help=( 

445 "Like --exclude, but adds additional files and directories on top of the" 

446 " default values instead of overriding them." 

447 ), 

448) 

449@click.option( 

450 "--force-exclude", 

451 type=str, 

452 callback=validate_regex, 

453 help=( 

454 "Like --exclude, but files and directories matching this regex will be excluded" 

455 " even when they are passed explicitly as arguments. This is useful when" 

456 " invoking Black programmatically on changed files, such as in a pre-commit" 

457 " hook or editor plugin." 

458 ), 

459) 

460@click.option( 

461 "--stdin-filename", 

462 type=str, 

463 is_eager=True, 

464 help=( 

465 "The name of the file when passing it through stdin. Useful to make sure Black" 

466 " will respect the --force-exclude option on some editors that rely on using" 

467 " stdin." 

468 ), 

469) 

470@click.option( 

471 "--include", 

472 type=str, 

473 default=DEFAULT_INCLUDES, 

474 callback=validate_regex, 

475 help=( 

476 "A regular expression that matches files and directories that should be" 

477 " included on recursive searches. An empty value means all files are included" 

478 " regardless of the name. Use forward slashes for directories on all platforms" 

479 " (Windows, too). Overrides all exclusions, including from .gitignore and" 

480 " command line options." 

481 ), 

482 show_default=True, 

483) 

484@click.option( 

485 "-W", 

486 "--workers", 

487 type=click.IntRange(min=1), 

488 default=None, 

489 help=( 

490 "When Black formats multiple files, it may use a process pool to speed up" 

491 " formatting. This option controls the number of parallel workers. This can" 

492 " also be specified via the BLACK_NUM_WORKERS environment variable. Defaults" 

493 " to the number of CPUs in the system." 

494 ), 

495) 

496@click.option( 

497 "-q", 

498 "--quiet", 

499 is_flag=True, 

500 help=( 

501 "Stop emitting all non-critical output. Error messages will still be emitted" 

502 " (which can silenced by 2>/dev/null)." 

503 ), 

504) 

505@click.option( 

506 "-v", 

507 "--verbose", 

508 is_flag=True, 

509 help=( 

510 "Emit messages about files that were not changed or were ignored due to" 

511 " exclusion patterns. If Black is using a configuration file, a message" 

512 " detailing which one it is using will be emitted." 

513 ), 

514) 

515@click.version_option( 

516 version=__version__, 

517 message=( 

518 f"%(prog)s, %(version)s (compiled: {'yes' if COMPILED else 'no'})\n" 

519 f"Python ({platform.python_implementation()}) {platform.python_version()}" 

520 ), 

521) 

522@click.argument( 

523 "src", 

524 nargs=-1, 

525 type=click.Path( 

526 exists=True, file_okay=True, dir_okay=True, readable=True, allow_dash=True 

527 ), 

528 is_eager=True, 

529 metavar="SRC ...", 

530) 

531@click.option( 

532 "--config", 

533 type=click.Path( 

534 exists=True, 

535 file_okay=True, 

536 dir_okay=False, 

537 readable=True, 

538 allow_dash=False, 

539 path_type=str, 

540 ), 

541 is_eager=True, 

542 callback=read_pyproject_toml, 

543 help="Read configuration options from a configuration file.", 

544) 

545@click.option( 

546 "--no-cache", 

547 is_flag=True, 

548 help=( 

549 "Skip reading and writing the cache, forcing Black to reformat all" 

550 " included files." 

551 ), 

552) 

553@click.pass_context 

554def main( 

555 ctx: click.Context, 

556 code: str | None, 

557 line_length: int, 

558 target_version: list[TargetVersion], 

559 check: bool, 

560 diff: bool, 

561 line_ranges: Sequence[str], 

562 color: bool, 

563 fast: bool, 

564 pyi: bool, 

565 ipynb: bool, 

566 python_cell_magics: Sequence[str], 

567 skip_source_first_line: bool, 

568 skip_string_normalization: bool, 

569 skip_magic_trailing_comma: bool, 

570 preview: bool, 

571 unstable: bool, 

572 enable_unstable_feature: list[Preview], 

573 quiet: bool, 

574 verbose: bool, 

575 required_version: str | None, 

576 include: Pattern[str], 

577 exclude: Pattern[str] | None, 

578 extend_exclude: Pattern[str] | None, 

579 force_exclude: Pattern[str] | None, 

580 stdin_filename: str | None, 

581 workers: int | None, 

582 src: tuple[str, ...], 

583 config: str | None, 

584 no_cache: bool, 

585) -> None: 

586 """The uncompromising code formatter.""" 

587 ctx.ensure_object(dict) 

588 

589 assert sys.version_info >= (3, 10), "Black requires Python 3.10+" 

590 if sys.version_info[:3] == (3, 12, 5): 

591 out( 

592 "Python 3.12.5 has a memory safety issue that can cause Black's " 

593 "AST safety checks to fail. " 

594 "Please upgrade to Python 3.12.6 or downgrade to Python 3.12.4" 

595 ) 

596 ctx.exit(1) 

597 

598 if src and code is not None: 

599 out( 

600 main.get_usage(ctx) 

601 + "\n\n'SRC' and 'code' cannot be passed simultaneously." 

602 ) 

603 ctx.exit(1) 

604 if not src and code is None: 

605 out(main.get_usage(ctx) + "\n\nOne of 'SRC' or 'code' is required.") 

606 ctx.exit(1) 

607 

608 # It doesn't do anything if --unstable is also passed, so just allow it. 

609 if enable_unstable_feature and not (preview or unstable): 

610 out( 

611 main.get_usage(ctx) 

612 + "\n\n'--enable-unstable-feature' requires '--preview'." 

613 ) 

614 ctx.exit(1) 

615 

616 root, method = ( 

617 find_project_root(src, stdin_filename) if code is None else (None, None) 

618 ) 

619 ctx.obj["root"] = root 

620 

621 if verbose: 

622 if root: 

623 out( 

624 f"Identified `{root}` as project root containing a {method}.", 

625 fg="blue", 

626 ) 

627 

628 if config: 

629 config_source = ctx.get_parameter_source("config") 

630 user_level_config = str(find_user_pyproject_toml()) 

631 if config == user_level_config: 

632 out( 

633 "Using configuration from user-level config at " 

634 f"'{user_level_config}'.", 

635 fg="blue", 

636 ) 

637 elif config_source in ( 

638 ParameterSource.DEFAULT, 

639 ParameterSource.DEFAULT_MAP, 

640 ): 

641 out("Using configuration from project root.", fg="blue") 

642 else: 

643 out(f"Using configuration in '{config}'.", fg="blue") 

644 if ctx.default_map: 

645 for param, value in ctx.default_map.items(): 

646 out(f"{param}: {value}") 

647 

648 error_msg = "Oh no! 💥 💔 💥" 

649 if ( 

650 required_version 

651 and required_version != __version__ 

652 and required_version != __version__.split(".")[0] 

653 ): 

654 err( 

655 f"{error_msg} The required version `{required_version}` does not match" 

656 f" the running version `{__version__}`!" 

657 ) 

658 ctx.exit(1) 

659 if ipynb and pyi: 

660 err("Cannot pass both `pyi` and `ipynb` flags!") 

661 ctx.exit(1) 

662 

663 write_back = WriteBack.from_configuration(check=check, diff=diff, color=color) 

664 if target_version: 

665 versions = set(target_version) 

666 else: 

667 # We'll autodetect later. 

668 versions = set() 

669 mode = Mode( 

670 target_versions=versions, 

671 line_length=line_length, 

672 is_pyi=pyi, 

673 is_ipynb=ipynb, 

674 skip_source_first_line=skip_source_first_line, 

675 string_normalization=not skip_string_normalization, 

676 magic_trailing_comma=not skip_magic_trailing_comma, 

677 preview=preview, 

678 unstable=unstable, 

679 python_cell_magics=set(python_cell_magics), 

680 enabled_features=set(enable_unstable_feature), 

681 ) 

682 

683 if not fast and _target_versions_exceed_runtime(versions): 

684 err( 

685 f"Warning: {_version_mismatch_message(versions)} Black's safety" 

686 " check verifies equivalence by parsing the AST, which fails" 

687 " when the running Python is older than the target version.", 

688 fg="yellow", 

689 ) 

690 

691 lines: list[tuple[int, int]] = [] 

692 if line_ranges: 

693 if ipynb: 

694 err("Cannot use --line-ranges with ipynb files.") 

695 ctx.exit(1) 

696 

697 try: 

698 lines = parse_line_ranges(line_ranges) 

699 except ValueError as e: 

700 err(str(e)) 

701 ctx.exit(1) 

702 

703 if code is not None: 

704 # Run in quiet mode by default with -c; the extra output isn't useful. 

705 # You can still pass -v to get verbose output. 

706 quiet = True 

707 

708 report = Report(check=check, diff=diff, quiet=quiet, verbose=verbose) 

709 

710 if code is not None: 

711 reformat_code( 

712 content=code, 

713 fast=fast, 

714 write_back=write_back, 

715 mode=mode, 

716 report=report, 

717 lines=lines, 

718 ) 

719 else: 

720 assert root is not None # root is only None if code is not None 

721 try: 

722 sources = get_sources( 

723 root=root, 

724 src=src, 

725 quiet=quiet, 

726 verbose=verbose, 

727 include=include, 

728 exclude=exclude, 

729 extend_exclude=extend_exclude, 

730 force_exclude=force_exclude, 

731 report=report, 

732 stdin_filename=stdin_filename, 

733 ) 

734 except GitIgnorePatternError: 

735 ctx.exit(1) 

736 

737 if not sources: 

738 if verbose or not quiet: 

739 out("No Python files are present to be formatted. Nothing to do 😴") 

740 if "-" in src: 

741 sys.stdout.write(sys.stdin.read()) 

742 ctx.exit(0) 

743 

744 if len(sources) == 1: 

745 reformat_one( 

746 src=sources.pop(), 

747 fast=fast, 

748 write_back=write_back, 

749 mode=mode, 

750 report=report, 

751 lines=lines, 

752 no_cache=no_cache, 

753 ) 

754 else: 

755 from black.concurrency import reformat_many 

756 

757 if lines: 

758 err("Cannot use --line-ranges to format multiple files.") 

759 ctx.exit(1) 

760 reformat_many( 

761 sources=sources, 

762 fast=fast, 

763 write_back=write_back, 

764 mode=mode, 

765 report=report, 

766 workers=workers, 

767 no_cache=no_cache, 

768 ) 

769 

770 if verbose or not quiet: 

771 if code is None and (verbose or report.change_count or report.failure_count): 

772 out() 

773 out(error_msg if report.return_code else "All done! ✨ 🍰 ✨") 

774 if code is None: 

775 click.echo(str(report), err=True) 

776 ctx.exit(report.return_code) 

777 

778 

779def get_sources( 

780 *, 

781 root: Path, 

782 src: tuple[str, ...], 

783 quiet: bool, 

784 verbose: bool, 

785 include: Pattern[str], 

786 exclude: Pattern[str] | None, 

787 extend_exclude: Pattern[str] | None, 

788 force_exclude: Pattern[str] | None, 

789 report: "Report", 

790 stdin_filename: str | None, 

791) -> set[Path]: 

792 """Compute the set of files to be formatted.""" 

793 sources: set[Path] = set() 

794 

795 assert root.is_absolute(), f"INTERNAL ERROR: `root` must be absolute but is {root}" 

796 using_default_exclude = exclude is None 

797 exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES) if exclude is None else exclude 

798 gitignore: dict[Path, GitIgnoreSpec] | None = None 

799 root_gitignore = get_gitignore(root) 

800 

801 for s in src: 

802 if s == "-" and stdin_filename: 

803 path = Path(stdin_filename) 

804 if path_is_excluded(stdin_filename, force_exclude): 

805 report.path_ignored( 

806 path, 

807 "--stdin-filename matches the --force-exclude regular expression", 

808 ) 

809 continue 

810 is_stdin = True 

811 else: 

812 path = Path(s) 

813 is_stdin = False 

814 

815 # Compare the logic here to the logic in `gen_python_files`. 

816 if is_stdin or path.is_file(): 

817 if resolves_outside_root_or_cannot_stat(path, root, report): 

818 if verbose: 

819 out(f'Skipping invalid source: "{path}"', fg="red") 

820 continue 

821 

822 root_relative_path = best_effort_relative_path(path, root).as_posix() 

823 root_relative_path = "/" + root_relative_path 

824 

825 # Hard-exclude any files that matches the `--force-exclude` regex. 

826 if path_is_excluded(root_relative_path, force_exclude): 

827 report.path_ignored( 

828 path, "matches the --force-exclude regular expression" 

829 ) 

830 continue 

831 

832 if is_stdin: 

833 path = Path(f"{STDIN_PLACEHOLDER}{path}") 

834 

835 if path.suffix == ".ipynb" and not jupyter_dependencies_are_installed( 

836 warn=verbose or not quiet 

837 ): 

838 continue 

839 

840 if verbose: 

841 out(f'Found input source: "{path}"', fg="blue") 

842 sources.add(path) 

843 elif path.is_dir(): 

844 path = root / (path.resolve().relative_to(root)) 

845 if verbose: 

846 out(f'Found input source directory: "{path}"', fg="blue") 

847 

848 if using_default_exclude: 

849 gitignore = { 

850 root: root_gitignore, 

851 path: get_gitignore(path), 

852 } 

853 sources.update( 

854 gen_python_files( 

855 path.iterdir(), 

856 root, 

857 include, 

858 exclude, 

859 extend_exclude, 

860 force_exclude, 

861 report, 

862 gitignore, 

863 verbose=verbose, 

864 quiet=quiet, 

865 ) 

866 ) 

867 elif s == "-": 

868 if verbose: 

869 out("Found input source stdin", fg="blue") 

870 sources.add(path) 

871 else: 

872 err(f"invalid path: {s}") 

873 

874 return sources 

875 

876 

877def reformat_code( 

878 content: str, 

879 fast: bool, 

880 write_back: WriteBack, 

881 mode: Mode, 

882 report: Report, 

883 *, 

884 lines: Collection[tuple[int, int]] = (), 

885) -> None: 

886 """ 

887 Reformat and print out `content` without spawning child processes. 

888 Similar to `reformat_one`, but for string content. 

889 

890 `fast`, `write_back`, and `mode` options are passed to 

891 :func:`format_file_in_place` or :func:`format_stdin_to_stdout`. 

892 """ 

893 path = Path("<string>") 

894 try: 

895 changed = Changed.NO 

896 if format_stdin_to_stdout( 

897 content=content, fast=fast, write_back=write_back, mode=mode, lines=lines 

898 ): 

899 changed = Changed.YES 

900 report.done(path, changed) 

901 except Exception as exc: 

902 if report.verbose: 

903 traceback.print_exc() 

904 report.failed(path, str(exc)) 

905 

906 

907# diff-shades depends on being to monkeypatch this function to operate. I know it's 

908# not ideal, but this shouldn't cause any issues ... hopefully. ~ichard26 

909@mypyc_attr(patchable=True) 

910def reformat_one( 

911 src: Path, 

912 fast: bool, 

913 write_back: WriteBack, 

914 mode: Mode, 

915 report: "Report", 

916 *, 

917 lines: Collection[tuple[int, int]] = (), 

918 no_cache: bool = False, 

919) -> None: 

920 """Reformat a single file under `src` without spawning child processes. 

921 

922 `fast`, `write_back`, and `mode` options are passed to 

923 :func:`format_file_in_place` or :func:`format_stdin_to_stdout`. 

924 """ 

925 try: 

926 changed = Changed.NO 

927 

928 if str(src) == "-": 

929 is_stdin = True 

930 elif str(src).startswith(STDIN_PLACEHOLDER): 

931 is_stdin = True 

932 # Use the original name again in case we want to print something 

933 # to the user 

934 src = Path(str(src)[len(STDIN_PLACEHOLDER) :]) 

935 else: 

936 is_stdin = False 

937 

938 if is_stdin: 

939 if src.suffix == ".pyi": 

940 mode = replace(mode, is_pyi=True) 

941 elif src.suffix == ".ipynb": 

942 mode = replace(mode, is_ipynb=True) 

943 if format_stdin_to_stdout( 

944 fast=fast, write_back=write_back, mode=mode, lines=lines 

945 ): 

946 changed = Changed.YES 

947 else: 

948 cache = None if no_cache else Cache.read(mode) 

949 if cache is not None and write_back not in ( 

950 WriteBack.DIFF, 

951 WriteBack.COLOR_DIFF, 

952 ): 

953 if not cache.is_changed(src): 

954 changed = Changed.CACHED 

955 if changed is not Changed.CACHED and format_file_in_place( 

956 src, fast=fast, write_back=write_back, mode=mode, lines=lines 

957 ): 

958 changed = Changed.YES 

959 if cache is not None and ( 

960 (write_back is WriteBack.YES and changed is not Changed.CACHED) 

961 or (write_back is WriteBack.CHECK and changed is Changed.NO) 

962 ): 

963 cache.write([src]) 

964 report.done(src, changed) 

965 except Exception as exc: 

966 if report.verbose: 

967 traceback.print_exc() 

968 report.failed(src, str(exc)) 

969 

970 

971def format_file_in_place( 

972 src: Path, 

973 fast: bool, 

974 mode: Mode, 

975 write_back: WriteBack = WriteBack.NO, 

976 lock: Any = None, # multiprocessing.Manager().Lock() is some crazy proxy 

977 *, 

978 lines: Collection[tuple[int, int]] = (), 

979) -> bool: 

980 """Format file under `src` path. Return True if changed. 

981 

982 If `write_back` is DIFF, write a diff to stdout. If it is YES, write reformatted 

983 code to the file. 

984 `mode` and `fast` options are passed to :func:`format_file_contents`. 

985 """ 

986 if src.suffix == ".pyi": 

987 mode = replace(mode, is_pyi=True) 

988 elif src.suffix == ".ipynb": 

989 mode = replace(mode, is_ipynb=True) 

990 

991 then = datetime.fromtimestamp(src.stat().st_mtime, timezone.utc) 

992 header = b"" 

993 with open(src, "rb") as buf: 

994 if mode.skip_source_first_line: 

995 header = buf.readline() 

996 src_contents, encoding, newline = decode_bytes(buf.read(), mode) 

997 try: 

998 dst_contents = format_file_contents( 

999 src_contents, fast=fast, mode=mode, lines=lines 

1000 ) 

1001 except NothingChanged: 

1002 return False 

1003 except JSONDecodeError: 

1004 raise ValueError( 

1005 f"File '{src}' cannot be parsed as valid Jupyter notebook." 

1006 ) from None 

1007 src_contents = header.decode(encoding) + src_contents 

1008 dst_contents = header.decode(encoding) + dst_contents 

1009 

1010 if write_back == WriteBack.YES: 

1011 with open(src, "w", encoding=encoding, newline=newline) as f: 

1012 f.write(dst_contents) 

1013 elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF): 

1014 now = datetime.now(timezone.utc) 

1015 src_name = f"{src}\t{then}" 

1016 dst_name = f"{src}\t{now}" 

1017 if mode.is_ipynb: 

1018 diff_contents = ipynb_diff(src_contents, dst_contents, src_name, dst_name) 

1019 else: 

1020 diff_contents = diff(src_contents, dst_contents, src_name, dst_name) 

1021 

1022 if write_back == WriteBack.COLOR_DIFF: 

1023 diff_contents = color_diff(diff_contents) 

1024 

1025 with lock or nullcontext(): 

1026 f = io.TextIOWrapper( 

1027 sys.stdout.buffer, 

1028 encoding=encoding, 

1029 newline=newline, 

1030 write_through=True, 

1031 ) 

1032 f = wrap_stream_for_windows(f) 

1033 f.write(diff_contents) 

1034 f.detach() 

1035 

1036 return True 

1037 

1038 

1039def format_stdin_to_stdout( 

1040 fast: bool, 

1041 *, 

1042 content: str | None = None, 

1043 write_back: WriteBack = WriteBack.NO, 

1044 mode: Mode, 

1045 lines: Collection[tuple[int, int]] = (), 

1046) -> bool: 

1047 """Format file on stdin. Return True if changed. 

1048 

1049 If content is None, it's read from sys.stdin. 

1050 

1051 If `write_back` is YES, write reformatted code back to stdout. If it is DIFF, 

1052 write a diff to stdout. The `mode` argument is passed to 

1053 :func:`format_file_contents`. 

1054 """ 

1055 then = datetime.now(timezone.utc) 

1056 

1057 if content is None: 

1058 src, encoding, newline = decode_bytes(sys.stdin.buffer.read(), mode) 

1059 else: 

1060 src, encoding, newline = content, "utf-8", "\n" 

1061 

1062 dst = src 

1063 try: 

1064 dst = format_file_contents(src, fast=fast, mode=mode, lines=lines) 

1065 return True 

1066 

1067 except NothingChanged: 

1068 return False 

1069 

1070 finally: 

1071 f = io.TextIOWrapper( 

1072 sys.stdout.buffer, encoding=encoding, newline=newline, write_through=True 

1073 ) 

1074 if write_back == WriteBack.YES: 

1075 # Make sure there's a newline after the content 

1076 if dst and dst[-1] != "\n" and dst[-1] != "\r": 

1077 dst += newline 

1078 f.write(dst) 

1079 elif write_back in (WriteBack.DIFF, WriteBack.COLOR_DIFF): 

1080 now = datetime.now(timezone.utc) 

1081 src_name = f"STDIN\t{then}" 

1082 dst_name = f"STDOUT\t{now}" 

1083 d = diff(src, dst, src_name, dst_name) 

1084 if write_back == WriteBack.COLOR_DIFF: 

1085 d = color_diff(d) 

1086 f = wrap_stream_for_windows(f) 

1087 f.write(d) 

1088 f.detach() 

1089 

1090 

1091def check_stability_and_equivalence( 

1092 src_contents: str, 

1093 dst_contents: str, 

1094 *, 

1095 mode: Mode, 

1096 lines: Collection[tuple[int, int]] = (), 

1097) -> None: 

1098 """Perform stability and equivalence checks. 

1099 

1100 Raise AssertionError if source and destination contents are not 

1101 equivalent, or if a second pass of the formatter would format the 

1102 content differently. 

1103 """ 

1104 try: 

1105 assert_equivalent(src_contents, dst_contents) 

1106 except SourceASTParseError: 

1107 raise 

1108 except ASTSafetyError: 

1109 if _target_versions_exceed_runtime(mode.target_versions): 

1110 raise ASTSafetyError( 

1111 "failed to verify equivalence of the formatted output:" 

1112 f" {_version_mismatch_message(mode.target_versions)}" 

1113 ) from None 

1114 raise 

1115 assert_stable(src_contents, dst_contents, mode=mode, lines=lines) 

1116 

1117 

1118def format_file_contents( 

1119 src_contents: str, 

1120 *, 

1121 fast: bool, 

1122 mode: Mode, 

1123 lines: Collection[tuple[int, int]] = (), 

1124) -> FileContent: 

1125 """Reformat contents of a file and return new contents. 

1126 

1127 If `fast` is False, additionally confirm that the reformatted code is 

1128 valid by calling :func:`assert_equivalent` and :func:`assert_stable` on it. 

1129 `mode` is passed to :func:`format_str`. 

1130 """ 

1131 if mode.is_ipynb: 

1132 dst_contents = format_ipynb_string(src_contents, fast=fast, mode=mode) 

1133 else: 

1134 dst_contents = format_str(src_contents, mode=mode, lines=lines) 

1135 if src_contents == dst_contents: 

1136 raise NothingChanged 

1137 

1138 if not fast and not mode.is_ipynb: 

1139 # Jupyter notebooks will already have been checked above. 

1140 check_stability_and_equivalence( 

1141 src_contents, dst_contents, mode=mode, lines=lines 

1142 ) 

1143 return dst_contents 

1144 

1145 

1146def format_cell(src: str, *, fast: bool, mode: Mode) -> str: 

1147 """Format code in given cell of Jupyter notebook. 

1148 

1149 General idea is: 

1150 

1151 - if cell has trailing semicolon, remove it; 

1152 - if cell has IPython magics, mask them; 

1153 - format cell; 

1154 - reinstate IPython magics; 

1155 - reinstate trailing semicolon (if originally present); 

1156 - strip trailing newlines. 

1157 

1158 Cells with syntax errors will not be processed, as they 

1159 could potentially be automagics or multi-line magics, which 

1160 are currently not supported. 

1161 """ 

1162 validate_cell(src, mode) 

1163 src_without_trailing_semicolon, has_trailing_semicolon = remove_trailing_semicolon( 

1164 src 

1165 ) 

1166 try: 

1167 masked_src, replacements = mask_cell(src_without_trailing_semicolon) 

1168 except SyntaxError: 

1169 raise NothingChanged from None 

1170 masked_dst = format_str(masked_src, mode=mode) 

1171 if not fast: 

1172 check_stability_and_equivalence(masked_src, masked_dst, mode=mode) 

1173 dst_without_trailing_semicolon = unmask_cell(masked_dst, replacements) 

1174 dst = put_trailing_semicolon_back( 

1175 dst_without_trailing_semicolon, has_trailing_semicolon 

1176 ) 

1177 dst = dst.rstrip("\n") 

1178 if dst == src: 

1179 raise NothingChanged from None 

1180 return dst 

1181 

1182 

1183def validate_metadata(nb: MutableMapping[str, Any]) -> None: 

1184 """If notebook is marked as non-Python, don't format it. 

1185 

1186 All notebook metadata fields are optional, see 

1187 https://nbformat.readthedocs.io/en/stable/format_description.html. So 

1188 if a notebook has empty metadata, we will try to parse it anyway. 

1189 """ 

1190 language = nb.get("metadata", {}).get("language_info", {}).get("name", None) 

1191 if language is not None and language != "python": 

1192 raise NothingChanged from None 

1193 

1194 

1195def format_ipynb_string(src_contents: str, *, fast: bool, mode: Mode) -> FileContent: 

1196 """Format Jupyter notebook. 

1197 

1198 Operate cell-by-cell, only on code cells, only for Python notebooks. 

1199 If the ``.ipynb`` originally had a trailing newline, it'll be preserved. 

1200 """ 

1201 if not src_contents: 

1202 raise NothingChanged 

1203 

1204 trailing_newline = src_contents[-1] == "\n" 

1205 modified = False 

1206 nb = json.loads(src_contents) 

1207 validate_metadata(nb) 

1208 for cell in nb["cells"]: 

1209 if cell.get("cell_type", None) == "code": 

1210 try: 

1211 src = "".join(cell["source"]) 

1212 dst = format_cell(src, fast=fast, mode=mode) 

1213 except NothingChanged: 

1214 pass 

1215 else: 

1216 cell["source"] = dst.splitlines(keepends=True) 

1217 modified = True 

1218 if modified: 

1219 dst_contents = json.dumps(nb, indent=1, ensure_ascii=False) 

1220 if trailing_newline: 

1221 dst_contents = dst_contents + "\n" 

1222 return dst_contents 

1223 else: 

1224 raise NothingChanged 

1225 

1226 

1227def format_str( 

1228 src_contents: str, *, mode: Mode, lines: Collection[tuple[int, int]] = () 

1229) -> str: 

1230 """Reformat a string and return new contents. 

1231 

1232 `mode` determines formatting options, such as how many characters per line are 

1233 allowed. Example: 

1234 

1235 >>> import black 

1236 >>> print(black.format_str("def f(arg:str='')->None:...", mode=black.Mode())) 

1237 def f(arg: str = "") -> None: 

1238 ... 

1239 

1240 A more complex example: 

1241 

1242 >>> print( 

1243 ... black.format_str( 

1244 ... "def f(arg:str='')->None: hey", 

1245 ... mode=black.Mode( 

1246 ... target_versions={black.TargetVersion.PY36}, 

1247 ... line_length=10, 

1248 ... string_normalization=False, 

1249 ... is_pyi=False, 

1250 ... ), 

1251 ... ), 

1252 ... ) 

1253 def f( 

1254 arg: str = '', 

1255 ) -> None: 

1256 hey 

1257 

1258 """ 

1259 if lines: 

1260 lines = sanitized_lines(lines, src_contents) 

1261 if not lines: 

1262 return src_contents # Nothing to format 

1263 dst_contents = _format_str_once(src_contents, mode=mode, lines=lines) 

1264 # Forced second pass to work around optional trailing commas (becoming 

1265 # forced trailing commas on pass 2) interacting differently with optional 

1266 # parentheses. Admittedly ugly. 

1267 if src_contents != dst_contents: 

1268 if lines: 

1269 lines = adjusted_lines(lines, src_contents, dst_contents) 

1270 return _format_str_once(dst_contents, mode=mode, lines=lines) 

1271 return dst_contents 

1272 

1273 

1274def _format_str_once( 

1275 src_contents: str, *, mode: Mode, lines: Collection[tuple[int, int]] = () 

1276) -> str: 

1277 # Use the encoding overwrite since the src_contents may contain a different 

1278 # magic encoding comment than utf-8 

1279 normalized_contents, _, newline_type = decode_bytes( 

1280 src_contents.encode("utf-8"), mode, encoding_overwrite="utf-8" 

1281 ) 

1282 

1283 src_node = lib2to3_parse( 

1284 normalized_contents.lstrip(), target_versions=mode.target_versions 

1285 ) 

1286 

1287 dst_blocks: list[LinesBlock] = [] 

1288 if mode.target_versions: 

1289 versions = mode.target_versions 

1290 else: 

1291 future_imports = get_future_imports(src_node) 

1292 versions = detect_target_versions(src_node, future_imports=future_imports) 

1293 

1294 line_generation_features = { 

1295 feature 

1296 for feature in { 

1297 Feature.PARENTHESIZED_CONTEXT_MANAGERS, 

1298 Feature.UNPARENTHESIZED_EXCEPT_TYPES, 

1299 Feature.T_STRINGS, 

1300 } 

1301 if supports_feature(versions, feature) 

1302 } 

1303 normalize_fmt_off(src_node, mode, lines) 

1304 if lines: 

1305 # This should be called after normalize_fmt_off. 

1306 convert_unchanged_lines(src_node, lines) 

1307 

1308 line_generator = LineGenerator(mode=mode, features=line_generation_features) 

1309 elt = EmptyLineTracker(mode=mode) 

1310 split_line_features = { 

1311 feature 

1312 for feature in { 

1313 Feature.TRAILING_COMMA_IN_CALL, 

1314 Feature.TRAILING_COMMA_IN_DEF, 

1315 } 

1316 if supports_feature(versions, feature) 

1317 } 

1318 block: LinesBlock | None = None 

1319 for current_line in line_generator.visit(src_node): 

1320 block = elt.maybe_empty_lines(current_line) 

1321 dst_blocks.append(block) 

1322 for line in transform_line( 

1323 current_line, mode=mode, features=split_line_features 

1324 ): 

1325 block.content_lines.append(str(line)) 

1326 if dst_blocks: 

1327 dst_blocks[-1].after = 0 

1328 dst_contents = [] 

1329 for block in dst_blocks: 

1330 dst_contents.extend(block.all_lines()) 

1331 if not dst_contents: 

1332 if "\n" in normalized_contents: 

1333 return newline_type 

1334 return "".join(dst_contents).replace("\n", newline_type) 

1335 

1336 

1337def decode_bytes( 

1338 src: bytes, mode: Mode, *, encoding_overwrite: str | None = None 

1339) -> tuple[FileContent, Encoding, NewLine]: 

1340 """Return a tuple of (decoded_contents, encoding, newline). 

1341 

1342 `newline` is either CRLF, LF, or CR, but `decoded_contents` is decoded with 

1343 universal newlines (i.e. only contains LF). 

1344 

1345 Use the keyword only encoding_overwrite argument if the bytes are encoded 

1346 differently to their possible encoding magic comment. 

1347 """ 

1348 srcbuf = io.BytesIO(src) 

1349 

1350 # Still use detect encoding even if overwrite set because otherwise lines 

1351 # might be different 

1352 encoding, lines = tokenize.detect_encoding(srcbuf.readline) 

1353 if encoding_overwrite is not None: 

1354 encoding = encoding_overwrite 

1355 

1356 if not lines: 

1357 return "", encoding, "\n" 

1358 

1359 if lines[0][-2:] == b"\r\n": 

1360 if b"\r" in lines[0][:-2]: 

1361 newline = "\r" 

1362 else: 

1363 newline = "\r\n" 

1364 elif lines[0][-1:] == b"\n": 

1365 if b"\r" in lines[0][:-1]: 

1366 newline = "\r" 

1367 else: 

1368 newline = "\n" 

1369 else: 

1370 if b"\r" in lines[0]: 

1371 newline = "\r" 

1372 else: 

1373 newline = "\n" 

1374 

1375 srcbuf.seek(0) 

1376 with io.TextIOWrapper(srcbuf, encoding) as tiow: 

1377 return tiow.read(), encoding, newline 

1378 

1379 

1380def get_features_used( 

1381 node: Node, *, future_imports: set[str] | None = None 

1382) -> set[Feature]: 

1383 """Return a set of (relatively) new Python features used in this file. 

1384 

1385 Currently looking for: 

1386 - f-strings; 

1387 - self-documenting expressions in f-strings (f"{x=}"); 

1388 - underscores in numeric literals; 

1389 - trailing commas after * or ** in function signatures and calls; 

1390 - positional only arguments in function signatures and lambdas; 

1391 - assignment expression; 

1392 - relaxed decorator syntax; 

1393 - usage of __future__ flags (annotations); 

1394 - print / exec statements; 

1395 - parenthesized context managers; 

1396 - match statements; 

1397 - except* clause; 

1398 - variadic generics; 

1399 - lazy imports; 

1400 - starred or double-starred comprehensions. 

1401 """ 

1402 features: set[Feature] = set() 

1403 if future_imports: 

1404 features |= { 

1405 FUTURE_FLAG_TO_FEATURE[future_import] 

1406 for future_import in future_imports 

1407 if future_import in FUTURE_FLAG_TO_FEATURE 

1408 } 

1409 

1410 for n in node.pre_order(): 

1411 if n.type == token.FSTRING_START: 

1412 features.add(Feature.F_STRINGS) 

1413 elif n.type == token.TSTRING_START: 

1414 features.add(Feature.T_STRINGS) 

1415 elif ( 

1416 n.type == token.RBRACE 

1417 and n.parent is not None 

1418 and any(child.type == token.EQUAL for child in n.parent.children) 

1419 ): 

1420 features.add(Feature.DEBUG_F_STRINGS) 

1421 

1422 elif is_number_token(n): 

1423 if "_" in n.value: 

1424 features.add(Feature.NUMERIC_UNDERSCORES) 

1425 

1426 elif n.type == token.SLASH: 

1427 if n.parent and n.parent.type in { 

1428 syms.typedargslist, 

1429 syms.arglist, 

1430 syms.varargslist, 

1431 }: 

1432 features.add(Feature.POS_ONLY_ARGUMENTS) 

1433 

1434 elif n.type == token.COLONEQUAL: 

1435 features.add(Feature.ASSIGNMENT_EXPRESSIONS) 

1436 

1437 elif n.type == token.LAZY: 

1438 features.add(Feature.LAZY_IMPORTS) 

1439 

1440 elif n.type == syms.decorator: 

1441 if len(n.children) > 1 and not is_simple_decorator_expression( 

1442 n.children[1] 

1443 ): 

1444 features.add(Feature.RELAXED_DECORATORS) 

1445 

1446 elif is_unpacking_comprehension(n): 

1447 features.add(Feature.UNPACKING_IN_COMPREHENSIONS) 

1448 

1449 elif ( 

1450 n.type in {syms.typedargslist, syms.arglist} 

1451 and n.children 

1452 and n.children[-1].type == token.COMMA 

1453 ): 

1454 if n.type == syms.typedargslist: 

1455 feature = Feature.TRAILING_COMMA_IN_DEF 

1456 else: 

1457 feature = Feature.TRAILING_COMMA_IN_CALL 

1458 

1459 for ch in n.children: 

1460 if ch.type in STARS: 

1461 features.add(feature) 

1462 

1463 if ch.type == syms.argument: 

1464 for argch in ch.children: 

1465 if argch.type in STARS: 

1466 features.add(feature) 

1467 

1468 elif ( 

1469 n.type in {syms.return_stmt, syms.yield_expr} 

1470 and len(n.children) >= 2 

1471 and n.children[1].type == syms.testlist_star_expr 

1472 and any(child.type == syms.star_expr for child in n.children[1].children) 

1473 ): 

1474 features.add(Feature.UNPACKING_ON_FLOW) 

1475 

1476 elif ( 

1477 n.type == syms.annassign 

1478 and len(n.children) >= 4 

1479 and n.children[3].type == syms.testlist_star_expr 

1480 ): 

1481 features.add(Feature.ANN_ASSIGN_EXTENDED_RHS) 

1482 

1483 elif ( 

1484 n.type == syms.with_stmt 

1485 and len(n.children) > 2 

1486 and n.children[1].type == syms.atom 

1487 ): 

1488 atom_children = n.children[1].children 

1489 if ( 

1490 len(atom_children) == 3 

1491 and atom_children[0].type == token.LPAR 

1492 and _contains_asexpr(atom_children[1]) 

1493 and atom_children[2].type == token.RPAR 

1494 ): 

1495 features.add(Feature.PARENTHESIZED_CONTEXT_MANAGERS) 

1496 

1497 elif n.type == syms.match_stmt: 

1498 features.add(Feature.PATTERN_MATCHING) 

1499 

1500 elif n.type in {syms.subscriptlist, syms.trailer} and any( 

1501 child.type == syms.star_expr for child in n.children 

1502 ): 

1503 features.add(Feature.VARIADIC_GENERICS) 

1504 

1505 elif ( 

1506 n.type == syms.tname_star 

1507 and len(n.children) == 3 

1508 and n.children[2].type == syms.star_expr 

1509 ): 

1510 features.add(Feature.VARIADIC_GENERICS) 

1511 

1512 elif n.type in (syms.type_stmt, syms.typeparams): 

1513 features.add(Feature.TYPE_PARAMS) 

1514 

1515 elif ( 

1516 n.type in (syms.typevartuple, syms.paramspec, syms.typevar) 

1517 and n.children[-2].type == token.EQUAL 

1518 ): 

1519 features.add(Feature.TYPE_PARAM_DEFAULTS) 

1520 

1521 elif ( 

1522 n.type == syms.except_clause 

1523 and len(n.children) >= 2 

1524 and ( 

1525 n.children[1].type == token.STAR or n.children[1].type == syms.testlist 

1526 ) 

1527 ): 

1528 is_star_except = n.children[1].type == token.STAR 

1529 

1530 if is_star_except: 

1531 features.add(Feature.EXCEPT_STAR) 

1532 

1533 # Presence of except* pushes as clause 1 index back 

1534 has_as_clause = ( 

1535 len(n.children) >= is_star_except + 3 

1536 and n.children[is_star_except + 2].type == token.NAME 

1537 and n.children[is_star_except + 2].value == "as" # type: ignore 

1538 ) 

1539 

1540 # If there's no 'as' clause and the except expression is a testlist. 

1541 if not has_as_clause and ( 

1542 (is_star_except and n.children[2].type == syms.testlist) 

1543 or (not is_star_except and n.children[1].type == syms.testlist) 

1544 ): 

1545 features.add(Feature.UNPARENTHESIZED_EXCEPT_TYPES) 

1546 

1547 return features 

1548 

1549 

1550def is_unpacking_comprehension(node: LN) -> bool: 

1551 if node.type not in {syms.listmaker, syms.testlist_gexp, syms.dictsetmaker}: 

1552 return False 

1553 

1554 if not any( 

1555 child.type in {syms.comp_for, syms.old_comp_for} for child in node.children 

1556 ): 

1557 return False 

1558 

1559 first_child = node.children[0] 

1560 return first_child.type == syms.star_expr or first_child.type == token.DOUBLESTAR 

1561 

1562 

1563def _contains_asexpr(node: Node | Leaf) -> bool: 

1564 """Return True if `node` contains an as-pattern.""" 

1565 if node.type == syms.asexpr_test: 

1566 return True 

1567 elif node.type == syms.atom: 

1568 if ( 

1569 len(node.children) == 3 

1570 and node.children[0].type == token.LPAR 

1571 and node.children[2].type == token.RPAR 

1572 ): 

1573 return _contains_asexpr(node.children[1]) 

1574 elif node.type == syms.testlist_gexp: 

1575 return any(_contains_asexpr(child) for child in node.children) 

1576 return False 

1577 

1578 

1579def detect_target_versions( 

1580 node: Node, *, future_imports: set[str] | None = None 

1581) -> set[TargetVersion]: 

1582 """Detect the version to target based on the nodes used.""" 

1583 features = get_features_used(node, future_imports=future_imports) 

1584 return { 

1585 version for version in TargetVersion if features <= VERSION_TO_FEATURES[version] 

1586 } 

1587 

1588 

1589def get_future_imports(node: Node) -> set[str]: 

1590 """Return a set of __future__ imports in the file.""" 

1591 imports: set[str] = set() 

1592 

1593 def get_imports_from_children(children: list[LN]) -> Generator[str, None, None]: 

1594 for child in children: 

1595 if isinstance(child, Leaf): 

1596 if child.type == token.NAME: 

1597 yield child.value 

1598 

1599 elif child.type == syms.import_as_name: 

1600 orig_name = child.children[0] 

1601 assert isinstance(orig_name, Leaf), "Invalid syntax parsing imports" 

1602 assert orig_name.type == token.NAME, "Invalid syntax parsing imports" 

1603 yield orig_name.value 

1604 

1605 elif child.type == syms.import_as_names: 

1606 yield from get_imports_from_children(child.children) 

1607 

1608 else: 

1609 raise AssertionError("Invalid syntax parsing imports") 

1610 

1611 for child in node.children: 

1612 if child.type != syms.simple_stmt: 

1613 break 

1614 

1615 first_child = child.children[0] 

1616 if isinstance(first_child, Leaf): 

1617 # Continue looking if we see a docstring; otherwise stop. 

1618 if ( 

1619 len(child.children) == 2 

1620 and first_child.type == token.STRING 

1621 and child.children[1].type == token.NEWLINE 

1622 ): 

1623 continue 

1624 

1625 break 

1626 

1627 elif first_child.type == syms.import_from: 

1628 if first_child.children[0].type == token.LAZY: 

1629 break 

1630 

1631 module_name = first_child.children[1] 

1632 if not isinstance(module_name, Leaf) or module_name.value != "__future__": 

1633 break 

1634 

1635 imports |= set(get_imports_from_children(first_child.children[3:])) 

1636 else: 

1637 break 

1638 

1639 return imports 

1640 

1641 

1642def _black_info() -> str: 

1643 return ( 

1644 f"Black {__version__} on " 

1645 f"Python ({platform.python_implementation()}) {platform.python_version()}" 

1646 ) 

1647 

1648 

1649def assert_equivalent(src: str, dst: str) -> None: 

1650 """Raise AssertionError if `src` and `dst` aren't equivalent.""" 

1651 try: 

1652 src_ast = parse_ast(src) 

1653 except Exception as exc: 

1654 raise SourceASTParseError( 

1655 "cannot use --safe with this file; failed to parse source file AST: " 

1656 f"{exc}\n" 

1657 "This could be caused by running Black with an older Python version " 

1658 "that does not support new syntax used in your source file." 

1659 ) from exc 

1660 

1661 try: 

1662 dst_ast = parse_ast(dst) 

1663 except Exception as exc: 

1664 log = dump_to_file("".join(traceback.format_tb(exc.__traceback__)), dst) 

1665 raise ASTSafetyError( 

1666 f"INTERNAL ERROR: {_black_info()} produced invalid code: {exc}. " 

1667 "Please report a bug on https://github.com/psf/black/issues. " 

1668 f"This invalid output might be helpful: {log}" 

1669 ) from None 

1670 

1671 src_ast_str = "\n".join(stringify_ast(src_ast)) 

1672 dst_ast_str = "\n".join(stringify_ast(dst_ast)) 

1673 if src_ast_str != dst_ast_str: 

1674 log = dump_to_file(diff(src_ast_str, dst_ast_str, "src", "dst")) 

1675 raise ASTSafetyError( 

1676 f"INTERNAL ERROR: {_black_info()} produced code that is not equivalent to" 

1677 " the source. Please report a bug on https://github.com/psf/black/issues." 

1678 f" This diff might be helpful: {log}" 

1679 ) from None 

1680 

1681 

1682def assert_stable( 

1683 src: str, dst: str, mode: Mode, *, lines: Collection[tuple[int, int]] = () 

1684) -> None: 

1685 """Raise AssertionError if `dst` reformats differently the second time.""" 

1686 if lines: 

1687 # Formatting specified lines requires `adjusted_lines` to map original lines 

1688 # to the formatted lines before re-formatting the previously formatted result. 

1689 # Due to less-ideal diff algorithm, some edge cases produce incorrect new line 

1690 # ranges. Hence for now, we skip the stable check. 

1691 # See https://github.com/psf/black/issues/4033 for context. 

1692 return 

1693 # We shouldn't call format_str() here, because that formats the string 

1694 # twice and may hide a bug where we bounce back and forth between two 

1695 # versions. 

1696 newdst = _format_str_once(dst, mode=mode, lines=lines) 

1697 if dst != newdst: 

1698 log = dump_to_file( 

1699 str(mode), 

1700 diff(src, dst, "source", "first pass"), 

1701 diff(dst, newdst, "first pass", "second pass"), 

1702 ) 

1703 raise AssertionError( 

1704 f"INTERNAL ERROR: {_black_info()} produced different code on the second" 

1705 " pass of the formatter. Please report a bug on" 

1706 f" https://github.com/psf/black/issues. This diff might be helpful: {log}" 

1707 ) from None 

1708 

1709 

1710def patched_main() -> None: 

1711 # PyInstaller patches multiprocessing to need freeze_support() even in non-Windows 

1712 # environments so just assume we always need to call it if frozen. 

1713 if getattr(sys, "frozen", False): 

1714 from multiprocessing import freeze_support 

1715 

1716 freeze_support() 

1717 

1718 main() 

1719 

1720 

1721if __name__ == "__main__": 

1722 patched_main()