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

636 statements  

1import io 

2import json 

3import platform 

4import re 

5import sys 

6import tokenize 

7import traceback 

8from collections.abc import ( 

9 Collection, 

10 Generator, 

11 Iterator, 

12 MutableMapping, 

13 Sequence, 

14 Sized, 

15) 

16from contextlib import contextmanager 

17from dataclasses import replace 

18from datetime import datetime, timezone 

19from enum import Enum 

20from json.decoder import JSONDecodeError 

21from pathlib import Path 

22from re import Pattern 

23from typing import Any, Optional, Union 

24 

25import click 

26from click.core import ParameterSource 

27from mypy_extensions import mypyc_attr 

28from pathspec import PathSpec 

29from pathspec.patterns.gitwildmatch import GitWildMatchPatternError 

30 

31from _black_version import version as __version__ 

32from black.cache import Cache 

33from black.comments import normalize_fmt_off 

34from black.const import ( 

35 DEFAULT_EXCLUDES, 

36 DEFAULT_INCLUDES, 

37 DEFAULT_LINE_LENGTH, 

38 STDIN_PLACEHOLDER, 

39) 

40from black.files import ( 

41 best_effort_relative_path, 

42 find_project_root, 

43 find_pyproject_toml, 

44 find_user_pyproject_toml, 

45 gen_python_files, 

46 get_gitignore, 

47 parse_pyproject_toml, 

48 path_is_excluded, 

49 resolves_outside_root_or_cannot_stat, 

50 wrap_stream_for_windows, 

51) 

52from black.handle_ipynb_magics import ( 

53 PYTHON_CELL_MAGICS, 

54 jupyter_dependencies_are_installed, 

55 mask_cell, 

56 put_trailing_semicolon_back, 

57 remove_trailing_semicolon, 

58 unmask_cell, 

59 validate_cell, 

60) 

61from black.linegen import LN, LineGenerator, transform_line 

62from black.lines import EmptyLineTracker, LinesBlock 

63from black.mode import FUTURE_FLAG_TO_FEATURE, VERSION_TO_FEATURES, Feature 

64from black.mode import Mode as Mode # re-exported 

65from black.mode import Preview, TargetVersion, supports_feature 

66from black.nodes import STARS, is_number_token, is_simple_decorator_expression, syms 

67from black.output import color_diff, diff, dump_to_file, err, ipynb_diff, out 

68from black.parsing import ( # noqa F401 

69 ASTSafetyError, 

70 InvalidInput, 

71 lib2to3_parse, 

72 parse_ast, 

73 stringify_ast, 

74) 

75from black.ranges import ( 

76 adjusted_lines, 

77 convert_unchanged_lines, 

78 parse_line_ranges, 

79 sanitized_lines, 

80) 

81from black.report import Changed, NothingChanged, Report 

82from blib2to3.pgen2 import token 

83from blib2to3.pytree import Leaf, Node 

84 

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

86 

87# types 

88FileContent = str 

89Encoding = str 

90NewLine = str 

91 

92 

93class WriteBack(Enum): 

94 NO = 0 

95 YES = 1 

96 DIFF = 2 

97 CHECK = 3 

98 COLOR_DIFF = 4 

99 

100 @classmethod 

101 def from_configuration( 

102 cls, *, check: bool, diff: bool, color: bool = False 

103 ) -> "WriteBack": 

104 if check and not diff: 

105 return cls.CHECK 

106 

107 if diff and color: 

108 return cls.COLOR_DIFF 

109 

110 return cls.DIFF if diff else cls.YES 

111 

112 

113# Legacy name, left for integrations. 

114FileMode = Mode 

115 

116 

117def read_pyproject_toml( 

118 ctx: click.Context, param: click.Parameter, value: Optional[str] 

119) -> Optional[str]: 

120 """Inject Black configuration from "pyproject.toml" into defaults in `ctx`. 

121 

122 Returns the path to a successfully found and read configuration file, None 

123 otherwise. 

124 """ 

125 if not value: 

126 value = find_pyproject_toml( 

127 ctx.params.get("src", ()), ctx.params.get("stdin_filename", None) 

128 ) 

129 if value is None: 

130 return None 

131 

132 try: 

133 config = parse_pyproject_toml(value) 

134 except (OSError, ValueError) as e: 

135 raise click.FileError( 

136 filename=value, hint=f"Error reading configuration file: {e}" 

137 ) from None 

138 

139 if not config: 

140 return None 

141 else: 

142 spellcheck_pyproject_toml_keys(ctx, list(config), value) 

143 # Sanitize the values to be Click friendly. For more information please see: 

144 # https://github.com/psf/black/issues/1458 

145 # https://github.com/pallets/click/issues/1567 

146 config = { 

147 k: str(v) if not isinstance(v, (list, dict)) else v 

148 for k, v in config.items() 

149 } 

150 

151 target_version = config.get("target_version") 

152 if target_version is not None and not isinstance(target_version, list): 

153 raise click.BadOptionUsage( 

154 "target-version", "Config key target-version must be a list" 

155 ) 

156 

157 exclude = config.get("exclude") 

158 if exclude is not None and not isinstance(exclude, str): 

159 raise click.BadOptionUsage("exclude", "Config key exclude must be a string") 

160 

161 extend_exclude = config.get("extend_exclude") 

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

163 raise click.BadOptionUsage( 

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

165 ) 

166 

167 line_ranges = config.get("line_ranges") 

168 if line_ranges is not None: 

169 raise click.BadOptionUsage( 

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

171 ) 

172 

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

174 if ctx.default_map: 

175 default_map.update(ctx.default_map) 

176 default_map.update(config) 

177 

178 ctx.default_map = default_map 

179 return value 

180 

181 

182def spellcheck_pyproject_toml_keys( 

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

184) -> None: 

185 invalid_keys: list[str] = [] 

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

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

188 if invalid_keys: 

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

190 out( 

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

192 fg="red", 

193 ) 

194 

195 

196def target_version_option_callback( 

197 c: click.Context, p: Union[click.Option, click.Parameter], v: tuple[str, ...] 

198) -> list[TargetVersion]: 

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

200 

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

202 when it was a lambda, causing mypyc trouble. 

203 """ 

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

205 

206 

207def enable_unstable_feature_callback( 

208 c: click.Context, p: Union[click.Option, click.Parameter], v: tuple[str, ...] 

209) -> list[Preview]: 

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

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

212 

213 

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

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

216 

217 If it contains newlines, use verbose mode. 

218 """ 

219 if "\n" in regex: 

220 regex = "(?x)" + regex 

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

222 return compiled 

223 

224 

225def validate_regex( 

226 ctx: click.Context, 

227 param: click.Parameter, 

228 value: Optional[str], 

229) -> Optional[Pattern[str]]: 

230 try: 

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

232 except re.error as e: 

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

234 

235 

236@click.command( 

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

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

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

240 help="The uncompromising code formatter.", 

241) 

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

243@click.option( 

244 "-l", 

245 "--line-length", 

246 type=int, 

247 default=DEFAULT_LINE_LENGTH, 

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

249 show_default=True, 

250) 

251@click.option( 

252 "-t", 

253 "--target-version", 

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

255 callback=target_version_option_callback, 

256 multiple=True, 

257 help=( 

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

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

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

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

262 ), 

263) 

264@click.option( 

265 "--pyi", 

266 is_flag=True, 

267 help=( 

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

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

270 ), 

271) 

272@click.option( 

273 "--ipynb", 

274 is_flag=True, 

275 help=( 

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

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

278 ), 

279) 

280@click.option( 

281 "--python-cell-magics", 

282 multiple=True, 

283 help=( 

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

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

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

287 ), 

288 default=[], 

289) 

290@click.option( 

291 "-x", 

292 "--skip-source-first-line", 

293 is_flag=True, 

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

295) 

296@click.option( 

297 "-S", 

298 "--skip-string-normalization", 

299 is_flag=True, 

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

301) 

302@click.option( 

303 "-C", 

304 "--skip-magic-trailing-comma", 

305 is_flag=True, 

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

307) 

308@click.option( 

309 "--preview", 

310 is_flag=True, 

311 help=( 

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

313 " functionality in the next major release." 

314 ), 

315) 

316@click.option( 

317 "--unstable", 

318 is_flag=True, 

319 help=( 

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

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

322 " release. Implies --preview." 

323 ), 

324) 

325@click.option( 

326 "--enable-unstable-feature", 

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

328 callback=enable_unstable_feature_callback, 

329 multiple=True, 

330 help=( 

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

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

333 " or existence of any unstable features." 

334 ), 

335) 

336@click.option( 

337 "--check", 

338 is_flag=True, 

339 help=( 

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

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

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

343 ), 

344) 

345@click.option( 

346 "--diff", 

347 is_flag=True, 

348 help=( 

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

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

351 ), 

352) 

353@click.option( 

354 "--color/--no-color", 

355 is_flag=True, 

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

357) 

358@click.option( 

359 "--line-ranges", 

360 multiple=True, 

361 metavar="START-END", 

362 help=( 

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

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

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

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

367 " inclusive on both ends." 

368 ), 

369 default=(), 

370) 

371@click.option( 

372 "--fast/--safe", 

373 is_flag=True, 

374 help=( 

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

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

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

378 ), 

379) 

380@click.option( 

381 "--required-version", 

382 type=str, 

383 help=( 

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

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

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

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

388 " results across environments." 

389 ), 

390) 

391@click.option( 

392 "--exclude", 

393 type=str, 

394 callback=validate_regex, 

395 help=( 

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

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

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

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

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

401 ), 

402 show_default=False, 

403) 

404@click.option( 

405 "--extend-exclude", 

406 type=str, 

407 callback=validate_regex, 

408 help=( 

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

410 " default values instead of overriding them." 

411 ), 

412) 

413@click.option( 

414 "--force-exclude", 

415 type=str, 

416 callback=validate_regex, 

417 help=( 

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

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

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

421 " hook or editor plugin." 

422 ), 

423) 

424@click.option( 

425 "--stdin-filename", 

426 type=str, 

427 is_eager=True, 

428 help=( 

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

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

431 " stdin." 

432 ), 

433) 

434@click.option( 

435 "--include", 

436 type=str, 

437 default=DEFAULT_INCLUDES, 

438 callback=validate_regex, 

439 help=( 

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

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

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

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

444 " command line options." 

445 ), 

446 show_default=True, 

447) 

448@click.option( 

449 "-W", 

450 "--workers", 

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

452 default=None, 

453 help=( 

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

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

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

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

458 ), 

459) 

460@click.option( 

461 "-q", 

462 "--quiet", 

463 is_flag=True, 

464 help=( 

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

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

467 ), 

468) 

469@click.option( 

470 "-v", 

471 "--verbose", 

472 is_flag=True, 

473 help=( 

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

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

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

477 ), 

478) 

479@click.version_option( 

480 version=__version__, 

481 message=( 

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

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

484 ), 

485) 

486@click.argument( 

487 "src", 

488 nargs=-1, 

489 type=click.Path( 

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

491 ), 

492 is_eager=True, 

493 metavar="SRC ...", 

494) 

495@click.option( 

496 "--config", 

497 type=click.Path( 

498 exists=True, 

499 file_okay=True, 

500 dir_okay=False, 

501 readable=True, 

502 allow_dash=False, 

503 path_type=str, 

504 ), 

505 is_eager=True, 

506 callback=read_pyproject_toml, 

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

508) 

509@click.pass_context 

510def main( # noqa: C901 

511 ctx: click.Context, 

512 code: Optional[str], 

513 line_length: int, 

514 target_version: list[TargetVersion], 

515 check: bool, 

516 diff: bool, 

517 line_ranges: Sequence[str], 

518 color: bool, 

519 fast: bool, 

520 pyi: bool, 

521 ipynb: bool, 

522 python_cell_magics: Sequence[str], 

523 skip_source_first_line: bool, 

524 skip_string_normalization: bool, 

525 skip_magic_trailing_comma: bool, 

526 preview: bool, 

527 unstable: bool, 

528 enable_unstable_feature: list[Preview], 

529 quiet: bool, 

530 verbose: bool, 

531 required_version: Optional[str], 

532 include: Pattern[str], 

533 exclude: Optional[Pattern[str]], 

534 extend_exclude: Optional[Pattern[str]], 

535 force_exclude: Optional[Pattern[str]], 

536 stdin_filename: Optional[str], 

537 workers: Optional[int], 

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

539 config: Optional[str], 

540) -> None: 

541 """The uncompromising code formatter.""" 

542 ctx.ensure_object(dict) 

543 

544 assert sys.version_info >= (3, 9), "Black requires Python 3.9+" 

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

546 out( 

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

548 "AST safety checks to fail. " 

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

550 ) 

551 ctx.exit(1) 

552 

553 if src and code is not None: 

554 out( 

555 main.get_usage(ctx) 

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

557 ) 

558 ctx.exit(1) 

559 if not src and code is None: 

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

561 ctx.exit(1) 

562 

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

564 if enable_unstable_feature and not (preview or unstable): 

565 out( 

566 main.get_usage(ctx) 

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

568 ) 

569 ctx.exit(1) 

570 

571 root, method = ( 

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

573 ) 

574 ctx.obj["root"] = root 

575 

576 if verbose: 

577 if root: 

578 out( 

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

580 fg="blue", 

581 ) 

582 

583 if config: 

584 config_source = ctx.get_parameter_source("config") 

585 user_level_config = str(find_user_pyproject_toml()) 

586 if config == user_level_config: 

587 out( 

588 "Using configuration from user-level config at " 

589 f"'{user_level_config}'.", 

590 fg="blue", 

591 ) 

592 elif config_source in ( 

593 ParameterSource.DEFAULT, 

594 ParameterSource.DEFAULT_MAP, 

595 ): 

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

597 else: 

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

599 if ctx.default_map: 

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

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

602 

603 error_msg = "Oh no! 💥 💔 💥" 

604 if ( 

605 required_version 

606 and required_version != __version__ 

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

608 ): 

609 err( 

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

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

612 ) 

613 ctx.exit(1) 

614 if ipynb and pyi: 

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

616 ctx.exit(1) 

617 

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

619 if target_version: 

620 versions = set(target_version) 

621 else: 

622 # We'll autodetect later. 

623 versions = set() 

624 mode = Mode( 

625 target_versions=versions, 

626 line_length=line_length, 

627 is_pyi=pyi, 

628 is_ipynb=ipynb, 

629 skip_source_first_line=skip_source_first_line, 

630 string_normalization=not skip_string_normalization, 

631 magic_trailing_comma=not skip_magic_trailing_comma, 

632 preview=preview, 

633 unstable=unstable, 

634 python_cell_magics=set(python_cell_magics), 

635 enabled_features=set(enable_unstable_feature), 

636 ) 

637 

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

639 if line_ranges: 

640 if ipynb: 

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

642 ctx.exit(1) 

643 

644 try: 

645 lines = parse_line_ranges(line_ranges) 

646 except ValueError as e: 

647 err(str(e)) 

648 ctx.exit(1) 

649 

650 if code is not None: 

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

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

653 quiet = True 

654 

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

656 

657 if code is not None: 

658 reformat_code( 

659 content=code, 

660 fast=fast, 

661 write_back=write_back, 

662 mode=mode, 

663 report=report, 

664 lines=lines, 

665 ) 

666 else: 

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

668 try: 

669 sources = get_sources( 

670 root=root, 

671 src=src, 

672 quiet=quiet, 

673 verbose=verbose, 

674 include=include, 

675 exclude=exclude, 

676 extend_exclude=extend_exclude, 

677 force_exclude=force_exclude, 

678 report=report, 

679 stdin_filename=stdin_filename, 

680 ) 

681 except GitWildMatchPatternError: 

682 ctx.exit(1) 

683 

684 path_empty( 

685 sources, 

686 "No Python files are present to be formatted. Nothing to do 😴", 

687 quiet, 

688 verbose, 

689 ctx, 

690 ) 

691 

692 if len(sources) == 1: 

693 reformat_one( 

694 src=sources.pop(), 

695 fast=fast, 

696 write_back=write_back, 

697 mode=mode, 

698 report=report, 

699 lines=lines, 

700 ) 

701 else: 

702 from black.concurrency import reformat_many 

703 

704 if lines: 

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

706 ctx.exit(1) 

707 reformat_many( 

708 sources=sources, 

709 fast=fast, 

710 write_back=write_back, 

711 mode=mode, 

712 report=report, 

713 workers=workers, 

714 ) 

715 

716 if verbose or not quiet: 

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

718 out() 

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

720 if code is None: 

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

722 ctx.exit(report.return_code) 

723 

724 

725def get_sources( 

726 *, 

727 root: Path, 

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

729 quiet: bool, 

730 verbose: bool, 

731 include: Pattern[str], 

732 exclude: Optional[Pattern[str]], 

733 extend_exclude: Optional[Pattern[str]], 

734 force_exclude: Optional[Pattern[str]], 

735 report: "Report", 

736 stdin_filename: Optional[str], 

737) -> set[Path]: 

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

739 sources: set[Path] = set() 

740 

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

742 using_default_exclude = exclude is None 

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

744 gitignore: Optional[dict[Path, PathSpec]] = None 

745 root_gitignore = get_gitignore(root) 

746 

747 for s in src: 

748 if s == "-" and stdin_filename: 

749 path = Path(stdin_filename) 

750 if path_is_excluded(stdin_filename, force_exclude): 

751 report.path_ignored( 

752 path, 

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

754 ) 

755 continue 

756 is_stdin = True 

757 else: 

758 path = Path(s) 

759 is_stdin = False 

760 

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

762 if is_stdin or path.is_file(): 

763 if resolves_outside_root_or_cannot_stat(path, root, report): 

764 if verbose: 

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

766 continue 

767 

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

769 root_relative_path = "/" + root_relative_path 

770 

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

772 if path_is_excluded(root_relative_path, force_exclude): 

773 report.path_ignored( 

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

775 ) 

776 continue 

777 

778 if is_stdin: 

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

780 

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

782 warn=verbose or not quiet 

783 ): 

784 continue 

785 

786 if verbose: 

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

788 sources.add(path) 

789 elif path.is_dir(): 

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

791 if verbose: 

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

793 

794 if using_default_exclude: 

795 gitignore = { 

796 root: root_gitignore, 

797 path: get_gitignore(path), 

798 } 

799 sources.update( 

800 gen_python_files( 

801 path.iterdir(), 

802 root, 

803 include, 

804 exclude, 

805 extend_exclude, 

806 force_exclude, 

807 report, 

808 gitignore, 

809 verbose=verbose, 

810 quiet=quiet, 

811 ) 

812 ) 

813 elif s == "-": 

814 if verbose: 

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

816 sources.add(path) 

817 else: 

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

819 

820 return sources 

821 

822 

823def path_empty( 

824 src: Sized, msg: str, quiet: bool, verbose: bool, ctx: click.Context 

825) -> None: 

826 """ 

827 Exit if there is no `src` provided for formatting 

828 """ 

829 if not src: 

830 if verbose or not quiet: 

831 out(msg) 

832 ctx.exit(0) 

833 

834 

835def reformat_code( 

836 content: str, 

837 fast: bool, 

838 write_back: WriteBack, 

839 mode: Mode, 

840 report: Report, 

841 *, 

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

843) -> None: 

844 """ 

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

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

847 

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

849 :func:`format_file_in_place` or :func:`format_stdin_to_stdout`. 

850 """ 

851 path = Path("<string>") 

852 try: 

853 changed = Changed.NO 

854 if format_stdin_to_stdout( 

855 content=content, fast=fast, write_back=write_back, mode=mode, lines=lines 

856 ): 

857 changed = Changed.YES 

858 report.done(path, changed) 

859 except Exception as exc: 

860 if report.verbose: 

861 traceback.print_exc() 

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

863 

864 

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

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

867@mypyc_attr(patchable=True) 

868def reformat_one( 

869 src: Path, 

870 fast: bool, 

871 write_back: WriteBack, 

872 mode: Mode, 

873 report: "Report", 

874 *, 

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

876) -> None: 

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

878 

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

880 :func:`format_file_in_place` or :func:`format_stdin_to_stdout`. 

881 """ 

882 try: 

883 changed = Changed.NO 

884 

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

886 is_stdin = True 

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

888 is_stdin = True 

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

890 # to the user 

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

892 else: 

893 is_stdin = False 

894 

895 if is_stdin: 

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

897 mode = replace(mode, is_pyi=True) 

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

899 mode = replace(mode, is_ipynb=True) 

900 if format_stdin_to_stdout( 

901 fast=fast, write_back=write_back, mode=mode, lines=lines 

902 ): 

903 changed = Changed.YES 

904 else: 

905 cache = Cache.read(mode) 

906 if write_back not in (WriteBack.DIFF, WriteBack.COLOR_DIFF): 

907 if not cache.is_changed(src): 

908 changed = Changed.CACHED 

909 if changed is not Changed.CACHED and format_file_in_place( 

910 src, fast=fast, write_back=write_back, mode=mode, lines=lines 

911 ): 

912 changed = Changed.YES 

913 if (write_back is WriteBack.YES and changed is not Changed.CACHED) or ( 

914 write_back is WriteBack.CHECK and changed is Changed.NO 

915 ): 

916 cache.write([src]) 

917 report.done(src, changed) 

918 except Exception as exc: 

919 if report.verbose: 

920 traceback.print_exc() 

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

922 

923 

924def format_file_in_place( 

925 src: Path, 

926 fast: bool, 

927 mode: Mode, 

928 write_back: WriteBack = WriteBack.NO, 

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

930 *, 

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

932) -> bool: 

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

934 

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

936 code to the file. 

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

938 """ 

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 

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

945 header = b"" 

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

947 if mode.skip_source_first_line: 

948 header = buf.readline() 

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

950 try: 

951 dst_contents = format_file_contents( 

952 src_contents, fast=fast, mode=mode, lines=lines 

953 ) 

954 except NothingChanged: 

955 return False 

956 except JSONDecodeError: 

957 raise ValueError( 

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

959 ) from None 

960 src_contents = header.decode(encoding) + src_contents 

961 dst_contents = header.decode(encoding) + dst_contents 

962 

963 if write_back == WriteBack.YES: 

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

965 f.write(dst_contents) 

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

967 now = datetime.now(timezone.utc) 

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

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

970 if mode.is_ipynb: 

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

972 else: 

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

974 

975 if write_back == WriteBack.COLOR_DIFF: 

976 diff_contents = color_diff(diff_contents) 

977 

978 with lock or nullcontext(): 

979 f = io.TextIOWrapper( 

980 sys.stdout.buffer, 

981 encoding=encoding, 

982 newline=newline, 

983 write_through=True, 

984 ) 

985 f = wrap_stream_for_windows(f) 

986 f.write(diff_contents) 

987 f.detach() 

988 

989 return True 

990 

991 

992def format_stdin_to_stdout( 

993 fast: bool, 

994 *, 

995 content: Optional[str] = None, 

996 write_back: WriteBack = WriteBack.NO, 

997 mode: Mode, 

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

999) -> bool: 

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

1001 

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

1003 

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

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

1006 :func:`format_file_contents`. 

1007 """ 

1008 then = datetime.now(timezone.utc) 

1009 

1010 if content is None: 

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

1012 elif Preview.normalize_cr_newlines in mode: 

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

1014 else: 

1015 src, encoding, newline = content, "utf-8", "" 

1016 

1017 dst = src 

1018 try: 

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

1020 return True 

1021 

1022 except NothingChanged: 

1023 return False 

1024 

1025 finally: 

1026 f = io.TextIOWrapper( 

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

1028 ) 

1029 if write_back == WriteBack.YES: 

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

1031 if Preview.normalize_cr_newlines in mode: 

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

1033 dst += newline 

1034 else: 

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

1036 dst += "\n" 

1037 f.write(dst) 

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

1039 now = datetime.now(timezone.utc) 

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

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

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

1043 if write_back == WriteBack.COLOR_DIFF: 

1044 d = color_diff(d) 

1045 f = wrap_stream_for_windows(f) 

1046 f.write(d) 

1047 f.detach() 

1048 

1049 

1050def check_stability_and_equivalence( 

1051 src_contents: str, 

1052 dst_contents: str, 

1053 *, 

1054 mode: Mode, 

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

1056) -> None: 

1057 """Perform stability and equivalence checks. 

1058 

1059 Raise AssertionError if source and destination contents are not 

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

1061 content differently. 

1062 """ 

1063 assert_equivalent(src_contents, dst_contents) 

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

1065 

1066 

1067def format_file_contents( 

1068 src_contents: str, 

1069 *, 

1070 fast: bool, 

1071 mode: Mode, 

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

1073) -> FileContent: 

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

1075 

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

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

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

1079 """ 

1080 if mode.is_ipynb: 

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

1082 else: 

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

1084 if src_contents == dst_contents: 

1085 raise NothingChanged 

1086 

1087 if not fast and not mode.is_ipynb: 

1088 # Jupyter notebooks will already have been checked above. 

1089 check_stability_and_equivalence( 

1090 src_contents, dst_contents, mode=mode, lines=lines 

1091 ) 

1092 return dst_contents 

1093 

1094 

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

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

1097 

1098 General idea is: 

1099 

1100 - if cell has trailing semicolon, remove it; 

1101 - if cell has IPython magics, mask them; 

1102 - format cell; 

1103 - reinstate IPython magics; 

1104 - reinstate trailing semicolon (if originally present); 

1105 - strip trailing newlines. 

1106 

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

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

1109 are currently not supported. 

1110 """ 

1111 validate_cell(src, mode) 

1112 src_without_trailing_semicolon, has_trailing_semicolon = remove_trailing_semicolon( 

1113 src 

1114 ) 

1115 try: 

1116 masked_src, replacements = mask_cell(src_without_trailing_semicolon) 

1117 except SyntaxError: 

1118 raise NothingChanged from None 

1119 masked_dst = format_str(masked_src, mode=mode) 

1120 if not fast: 

1121 check_stability_and_equivalence(masked_src, masked_dst, mode=mode) 

1122 dst_without_trailing_semicolon = unmask_cell(masked_dst, replacements) 

1123 dst = put_trailing_semicolon_back( 

1124 dst_without_trailing_semicolon, has_trailing_semicolon 

1125 ) 

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

1127 if dst == src: 

1128 raise NothingChanged from None 

1129 return dst 

1130 

1131 

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

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

1134 

1135 All notebook metadata fields are optional, see 

1136 https://nbformat.readthedocs.io/en/latest/format_description.html. So 

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

1138 """ 

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

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

1141 raise NothingChanged from None 

1142 

1143 

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

1145 """Format Jupyter notebook. 

1146 

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

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

1149 """ 

1150 if not src_contents: 

1151 raise NothingChanged 

1152 

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

1154 modified = False 

1155 nb = json.loads(src_contents) 

1156 validate_metadata(nb) 

1157 for cell in nb["cells"]: 

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

1159 try: 

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

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

1162 except NothingChanged: 

1163 pass 

1164 else: 

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

1166 modified = True 

1167 if modified: 

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

1169 if trailing_newline: 

1170 dst_contents = dst_contents + "\n" 

1171 return dst_contents 

1172 else: 

1173 raise NothingChanged 

1174 

1175 

1176def format_str( 

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

1178) -> str: 

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

1180 

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

1182 allowed. Example: 

1183 

1184 >>> import black 

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

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

1187 ... 

1188 

1189 A more complex example: 

1190 

1191 >>> print( 

1192 ... black.format_str( 

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

1194 ... mode=black.Mode( 

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

1196 ... line_length=10, 

1197 ... string_normalization=False, 

1198 ... is_pyi=False, 

1199 ... ), 

1200 ... ), 

1201 ... ) 

1202 def f( 

1203 arg: str = '', 

1204 ) -> None: 

1205 hey 

1206 

1207 """ 

1208 if lines: 

1209 lines = sanitized_lines(lines, src_contents) 

1210 if not lines: 

1211 return src_contents # Nothing to format 

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

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

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

1215 # parentheses. Admittedly ugly. 

1216 if src_contents != dst_contents: 

1217 if lines: 

1218 lines = adjusted_lines(lines, src_contents, dst_contents) 

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

1220 return dst_contents 

1221 

1222 

1223def _format_str_once( 

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

1225) -> str: 

1226 if Preview.normalize_cr_newlines in mode: 

1227 normalized_contents, _, newline_type = decode_bytes( 

1228 src_contents.encode("utf-8"), mode 

1229 ) 

1230 

1231 src_node = lib2to3_parse( 

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

1233 ) 

1234 else: 

1235 src_node = lib2to3_parse(src_contents.lstrip(), mode.target_versions) 

1236 

1237 dst_blocks: list[LinesBlock] = [] 

1238 if mode.target_versions: 

1239 versions = mode.target_versions 

1240 else: 

1241 future_imports = get_future_imports(src_node) 

1242 versions = detect_target_versions(src_node, future_imports=future_imports) 

1243 

1244 line_generation_features = { 

1245 feature 

1246 for feature in { 

1247 Feature.PARENTHESIZED_CONTEXT_MANAGERS, 

1248 Feature.UNPARENTHESIZED_EXCEPT_TYPES, 

1249 } 

1250 if supports_feature(versions, feature) 

1251 } 

1252 normalize_fmt_off(src_node, mode, lines) 

1253 if lines: 

1254 # This should be called after normalize_fmt_off. 

1255 convert_unchanged_lines(src_node, lines) 

1256 

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

1258 elt = EmptyLineTracker(mode=mode) 

1259 split_line_features = { 

1260 feature 

1261 for feature in { 

1262 Feature.TRAILING_COMMA_IN_CALL, 

1263 Feature.TRAILING_COMMA_IN_DEF, 

1264 } 

1265 if supports_feature(versions, feature) 

1266 } 

1267 block: Optional[LinesBlock] = None 

1268 for current_line in line_generator.visit(src_node): 

1269 block = elt.maybe_empty_lines(current_line) 

1270 dst_blocks.append(block) 

1271 for line in transform_line( 

1272 current_line, mode=mode, features=split_line_features 

1273 ): 

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

1275 if dst_blocks: 

1276 dst_blocks[-1].after = 0 

1277 dst_contents = [] 

1278 for block in dst_blocks: 

1279 dst_contents.extend(block.all_lines()) 

1280 if not dst_contents: 

1281 if Preview.normalize_cr_newlines in mode: 

1282 if "\n" in normalized_contents: 

1283 return newline_type 

1284 else: 

1285 # Use decode_bytes to retrieve the correct source newline (CRLF or LF), 

1286 # and check if normalized_content has more than one line 

1287 normalized_content, _, newline = decode_bytes( 

1288 src_contents.encode("utf-8"), mode 

1289 ) 

1290 if "\n" in normalized_content: 

1291 return newline 

1292 return "" 

1293 if Preview.normalize_cr_newlines in mode: 

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

1295 else: 

1296 return "".join(dst_contents) 

1297 

1298 

1299def decode_bytes(src: bytes, mode: Mode) -> tuple[FileContent, Encoding, NewLine]: 

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

1301 

1302 `newline` is either CRLF or LF but `decoded_contents` is decoded with 

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

1304 """ 

1305 srcbuf = io.BytesIO(src) 

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

1307 if not lines: 

1308 return "", encoding, "\n" 

1309 

1310 if Preview.normalize_cr_newlines in mode: 

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

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

1313 newline = "\r" 

1314 else: 

1315 newline = "\r\n" 

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

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

1318 newline = "\r" 

1319 else: 

1320 newline = "\n" 

1321 else: 

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

1323 newline = "\r" 

1324 else: 

1325 newline = "\n" 

1326 else: 

1327 newline = "\r\n" if lines[0][-2:] == b"\r\n" else "\n" 

1328 

1329 srcbuf.seek(0) 

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

1331 return tiow.read(), encoding, newline 

1332 

1333 

1334def get_features_used( # noqa: C901 

1335 node: Node, *, future_imports: Optional[set[str]] = None 

1336) -> set[Feature]: 

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

1338 

1339 Currently looking for: 

1340 - f-strings; 

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

1342 - underscores in numeric literals; 

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

1344 - positional only arguments in function signatures and lambdas; 

1345 - assignment expression; 

1346 - relaxed decorator syntax; 

1347 - usage of __future__ flags (annotations); 

1348 - print / exec statements; 

1349 - parenthesized context managers; 

1350 - match statements; 

1351 - except* clause; 

1352 - variadic generics; 

1353 """ 

1354 features: set[Feature] = set() 

1355 if future_imports: 

1356 features |= { 

1357 FUTURE_FLAG_TO_FEATURE[future_import] 

1358 for future_import in future_imports 

1359 if future_import in FUTURE_FLAG_TO_FEATURE 

1360 } 

1361 

1362 for n in node.pre_order(): 

1363 if n.type == token.FSTRING_START: 

1364 features.add(Feature.F_STRINGS) 

1365 elif ( 

1366 n.type == token.RBRACE 

1367 and n.parent is not None 

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

1369 ): 

1370 features.add(Feature.DEBUG_F_STRINGS) 

1371 

1372 elif is_number_token(n): 

1373 if "_" in n.value: 

1374 features.add(Feature.NUMERIC_UNDERSCORES) 

1375 

1376 elif n.type == token.SLASH: 

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

1378 syms.typedargslist, 

1379 syms.arglist, 

1380 syms.varargslist, 

1381 }: 

1382 features.add(Feature.POS_ONLY_ARGUMENTS) 

1383 

1384 elif n.type == token.COLONEQUAL: 

1385 features.add(Feature.ASSIGNMENT_EXPRESSIONS) 

1386 

1387 elif n.type == syms.decorator: 

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

1389 n.children[1] 

1390 ): 

1391 features.add(Feature.RELAXED_DECORATORS) 

1392 

1393 elif ( 

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

1395 and n.children 

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

1397 ): 

1398 if n.type == syms.typedargslist: 

1399 feature = Feature.TRAILING_COMMA_IN_DEF 

1400 else: 

1401 feature = Feature.TRAILING_COMMA_IN_CALL 

1402 

1403 for ch in n.children: 

1404 if ch.type in STARS: 

1405 features.add(feature) 

1406 

1407 if ch.type == syms.argument: 

1408 for argch in ch.children: 

1409 if argch.type in STARS: 

1410 features.add(feature) 

1411 

1412 elif ( 

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

1414 and len(n.children) >= 2 

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

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

1417 ): 

1418 features.add(Feature.UNPACKING_ON_FLOW) 

1419 

1420 elif ( 

1421 n.type == syms.annassign 

1422 and len(n.children) >= 4 

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

1424 ): 

1425 features.add(Feature.ANN_ASSIGN_EXTENDED_RHS) 

1426 

1427 elif ( 

1428 n.type == syms.with_stmt 

1429 and len(n.children) > 2 

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

1431 ): 

1432 atom_children = n.children[1].children 

1433 if ( 

1434 len(atom_children) == 3 

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

1436 and _contains_asexpr(atom_children[1]) 

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

1438 ): 

1439 features.add(Feature.PARENTHESIZED_CONTEXT_MANAGERS) 

1440 

1441 elif n.type == syms.match_stmt: 

1442 features.add(Feature.PATTERN_MATCHING) 

1443 

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

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

1446 ): 

1447 features.add(Feature.VARIADIC_GENERICS) 

1448 

1449 elif ( 

1450 n.type == syms.tname_star 

1451 and len(n.children) == 3 

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

1453 ): 

1454 features.add(Feature.VARIADIC_GENERICS) 

1455 

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

1457 features.add(Feature.TYPE_PARAMS) 

1458 

1459 elif ( 

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

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

1462 ): 

1463 features.add(Feature.TYPE_PARAM_DEFAULTS) 

1464 

1465 elif ( 

1466 n.type == syms.except_clause 

1467 and len(n.children) >= 2 

1468 and ( 

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

1470 ) 

1471 ): 

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

1473 

1474 if is_star_except: 

1475 features.add(Feature.EXCEPT_STAR) 

1476 

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

1478 has_as_clause = ( 

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

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

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

1482 ) 

1483 

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

1485 if not has_as_clause and ( 

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

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

1488 ): 

1489 features.add(Feature.UNPARENTHESIZED_EXCEPT_TYPES) 

1490 

1491 return features 

1492 

1493 

1494def _contains_asexpr(node: Union[Node, Leaf]) -> bool: 

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

1496 if node.type == syms.asexpr_test: 

1497 return True 

1498 elif node.type == syms.atom: 

1499 if ( 

1500 len(node.children) == 3 

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

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

1503 ): 

1504 return _contains_asexpr(node.children[1]) 

1505 elif node.type == syms.testlist_gexp: 

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

1507 return False 

1508 

1509 

1510def detect_target_versions( 

1511 node: Node, *, future_imports: Optional[set[str]] = None 

1512) -> set[TargetVersion]: 

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

1514 features = get_features_used(node, future_imports=future_imports) 

1515 return { 

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

1517 } 

1518 

1519 

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

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

1522 imports: set[str] = set() 

1523 

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

1525 for child in children: 

1526 if isinstance(child, Leaf): 

1527 if child.type == token.NAME: 

1528 yield child.value 

1529 

1530 elif child.type == syms.import_as_name: 

1531 orig_name = child.children[0] 

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

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

1534 yield orig_name.value 

1535 

1536 elif child.type == syms.import_as_names: 

1537 yield from get_imports_from_children(child.children) 

1538 

1539 else: 

1540 raise AssertionError("Invalid syntax parsing imports") 

1541 

1542 for child in node.children: 

1543 if child.type != syms.simple_stmt: 

1544 break 

1545 

1546 first_child = child.children[0] 

1547 if isinstance(first_child, Leaf): 

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

1549 if ( 

1550 len(child.children) == 2 

1551 and first_child.type == token.STRING 

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

1553 ): 

1554 continue 

1555 

1556 break 

1557 

1558 elif first_child.type == syms.import_from: 

1559 module_name = first_child.children[1] 

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

1561 break 

1562 

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

1564 else: 

1565 break 

1566 

1567 return imports 

1568 

1569 

1570def _black_info() -> str: 

1571 return ( 

1572 f"Black {__version__} on " 

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

1574 ) 

1575 

1576 

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

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

1579 try: 

1580 src_ast = parse_ast(src) 

1581 except Exception as exc: 

1582 raise ASTSafetyError( 

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

1584 f"{exc}\n" 

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

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

1587 ) from exc 

1588 

1589 try: 

1590 dst_ast = parse_ast(dst) 

1591 except Exception as exc: 

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

1593 raise ASTSafetyError( 

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

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

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

1597 ) from None 

1598 

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

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

1601 if src_ast_str != dst_ast_str: 

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

1603 raise ASTSafetyError( 

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

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

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

1607 ) from None 

1608 

1609 

1610def assert_stable( 

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

1612) -> None: 

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

1614 if lines: 

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

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

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

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

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

1620 return 

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

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

1623 # versions. 

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

1625 if dst != newdst: 

1626 log = dump_to_file( 

1627 str(mode), 

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

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

1630 ) 

1631 raise AssertionError( 

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

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

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

1635 ) from None 

1636 

1637 

1638@contextmanager 

1639def nullcontext() -> Iterator[None]: 

1640 """Return an empty context manager. 

1641 

1642 To be used like `nullcontext` in Python 3.7. 

1643 """ 

1644 yield 

1645 

1646 

1647def patched_main() -> None: 

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

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

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

1651 from multiprocessing import freeze_support 

1652 

1653 freeze_support() 

1654 

1655 main() 

1656 

1657 

1658if __name__ == "__main__": 

1659 patched_main()