def get_gitignore(root: Path) -> PathSpec: """Return a PathSpec matching gitignore content if present.""" gitignore = root / ".gitignore" lines: List[str] = [] if gitignore.is_file(): with gitignore.open(encoding="utf-8") as gf: lines = gf.readlines() try: return PathSpec.from_lines("gitwildmatch", lines) except GitWildMatchPatternError as e: err(f"Could not parse {gitignore}: {e}") raise
def find_pyproject_toml(path_search_start: Tuple[str, ...]) -> Optional[str]: """Find the absolute filepath to a pyproject.toml if it exists""" path_project_root, _ = find_project_root(path_search_start) path_pyproject_toml = path_project_root / "pyproject.toml" if path_pyproject_toml.is_file(): return str(path_pyproject_toml) try: path_user_pyproject_toml = find_user_pyproject_toml() return (str(path_user_pyproject_toml) if path_user_pyproject_toml.is_file() else None) except PermissionError as e: # We do not have access to the user-level config directory, so ignore it. err(f"Ignoring user configuration directory due to {e!r}") return None
def assertFormatEqual(self, expected: str, actual: str) -> None: if actual != expected and not os.environ.get("SKIP_AST_PRINT"): bdv: DebugVisitor[Any] out("Expected tree:", fg="green") try: exp_node = black.lib2to3_parse(expected) bdv = DebugVisitor() list(bdv.visit(exp_node)) except Exception as ve: err(str(ve)) out("Actual tree:", fg="red") try: exp_node = black.lib2to3_parse(actual) bdv = DebugVisitor() list(bdv.visit(exp_node)) except Exception as ve: err(str(ve)) self.assertMultiLineEqual(expected, actual)
def _assert_format_equal(expected: str, actual: str) -> None: if actual != expected and not os.environ.get("SKIP_AST_PRINT"): bdv: DebugVisitor[Any] out("Expected tree:", fg="green") try: exp_node = black.lib2to3_parse(expected) bdv = DebugVisitor() list(bdv.visit(exp_node)) except Exception as ve: err(str(ve)) out("Actual tree:", fg="red") try: exp_node = black.lib2to3_parse(actual) bdv = DebugVisitor() list(bdv.visit(exp_node)) except Exception as ve: err(str(ve)) if actual != expected: out(diff(expected, actual, "expected", "actual")) assert actual == expected
def get_sources( *, ctx: click.Context, src: Tuple[str, ...], quiet: bool, verbose: bool, include: Pattern[str], exclude: Optional[Pattern[str]], extend_exclude: Optional[Pattern[str]], force_exclude: Optional[Pattern[str]], report: "Report", stdin_filename: Optional[str], ) -> Set[Path]: """Compute the set of files to be formatted.""" root = find_project_root(src) sources: Set[Path] = set() path_empty(src, "No Path provided. Nothing to do 😴", quiet, verbose, ctx) if exclude is None: exclude = re_compile_maybe_verbose(DEFAULT_EXCLUDES) gitignore = get_gitignore(root) else: gitignore = None for s in src: if s == "-" and stdin_filename: p = Path(stdin_filename) is_stdin = True else: p = Path(s) is_stdin = False if is_stdin or p.is_file(): normalized_path = normalize_path_maybe_ignore(p, root, report) if normalized_path is None: continue normalized_path = "/" + normalized_path # Hard-exclude any files that matches the `--force-exclude` regex. if force_exclude: force_exclude_match = force_exclude.search(normalized_path) else: force_exclude_match = None if force_exclude_match and force_exclude_match.group(0): report.path_ignored( p, "matches the --force-exclude regular expression") continue if is_stdin: p = Path(f"{STDIN_PLACEHOLDER}{str(p)}") if p.suffix == ".ipynb" and not jupyter_dependencies_are_installed( verbose=verbose, quiet=quiet): continue sources.add(p) elif p.is_dir(): sources.update( gen_python_files( p.iterdir(), root, include, exclude, extend_exclude, force_exclude, report, gitignore, verbose=verbose, quiet=quiet, )) elif s == "-": sources.add(p) else: err(f"invalid path: {s}") return sources
def main( ctx: click.Context, code: Optional[str], line_length: int, target_version: List[TargetVersion], check: bool, diff: bool, color: bool, fast: bool, pyi: bool, ipynb: bool, skip_string_normalization: bool, skip_magic_trailing_comma: bool, experimental_string_processing: bool, quiet: bool, verbose: bool, required_version: Optional[str], include: Pattern[str], exclude: Optional[Pattern[str]], extend_exclude: Optional[Pattern[str]], force_exclude: Optional[Pattern[str]], stdin_filename: Optional[str], workers: int, src: Tuple[str, ...], config: Optional[str], ) -> None: """The uncompromising code formatter.""" if config and verbose: out(f"Using configuration from {config}.", bold=False, fg="blue") error_msg = "Oh no! 💥 💔 💥" if required_version and required_version != __version__: err(f"{error_msg} The required version `{required_version}` does not match" f" the running version `{__version__}`!") ctx.exit(1) if ipynb and pyi: err("Cannot pass both `pyi` and `ipynb` flags!") ctx.exit(1) write_back = WriteBack.from_configuration(check=check, diff=diff, color=color) if target_version: versions = set(target_version) else: # We'll autodetect later. versions = set() mode = Mode( target_versions=versions, line_length=line_length, is_pyi=pyi, is_ipynb=ipynb, string_normalization=not skip_string_normalization, magic_trailing_comma=not skip_magic_trailing_comma, experimental_string_processing=experimental_string_processing, ) if code is not None: # Run in quiet mode by default with -c; the extra output isn't useful. # You can still pass -v to get verbose output. quiet = True report = Report(check=check, diff=diff, quiet=quiet, verbose=verbose) if code is not None: reformat_code(content=code, fast=fast, write_back=write_back, mode=mode, report=report) else: try: sources = get_sources( ctx=ctx, src=src, quiet=quiet, verbose=verbose, include=include, exclude=exclude, extend_exclude=extend_exclude, force_exclude=force_exclude, report=report, stdin_filename=stdin_filename, ) except GitWildMatchPatternError: ctx.exit(1) path_empty( sources, "No Python files are present to be formatted. Nothing to do 😴", quiet, verbose, ctx, ) if len(sources) == 1: reformat_one( src=sources.pop(), fast=fast, write_back=write_back, mode=mode, report=report, ) else: reformat_many( sources=sources, fast=fast, write_back=write_back, mode=mode, report=report, workers=workers, ) if verbose or not quiet: out(error_msg if report.return_code else "All done! ✨ 🍰 ✨") if code is None: click.echo(str(report), err=True) ctx.exit(report.return_code)
def format_str(src_contents: str, *, mode: Mode) -> FileContent: """Reformat a string and return new contents. `mode` determines formatting options, such as how many characters per line are allowed. Example: >>> import black >>> print(black.format_str("def f(arg:str='')->None:...", mode=black.Mode())) def f(arg: str = "") -> None: ... A more complex example: >>> print( ... black.format_str( ... "def f(arg:str='')->None: hey", ... mode=black.Mode( ... target_versions={black.TargetVersion.PY36}, ... line_length=10, ... string_normalization=False, ... is_pyi=False, ... ), ... ), ... ) def f( arg: str = '', ) -> None: hey """ src_node = lib2to3_parse(src_contents.lstrip(), mode.target_versions) dst_contents = [] future_imports = get_future_imports(src_node) if mode.target_versions: versions = mode.target_versions else: versions = detect_target_versions(src_node) # TODO: fully drop support and this code hopefully in January 2022 :D if TargetVersion.PY27 in mode.target_versions or versions == { TargetVersion.PY27 }: msg = ( "DEPRECATION: Python 2 support will be removed in the first stable release " "expected in January 2022.") err(msg, fg="yellow", bold=True) normalize_fmt_off(src_node) lines = LineGenerator( mode=mode, remove_u_prefix="unicode_literals" in future_imports or supports_feature(versions, Feature.UNICODE_LITERALS), ) elt = EmptyLineTracker(is_pyi=mode.is_pyi) empty_line = Line(mode=mode) after = 0 split_line_features = { feature for feature in {Feature.TRAILING_COMMA_IN_CALL, Feature.TRAILING_COMMA_IN_DEF} if supports_feature(versions, feature) } for current_line in lines.visit(src_node): dst_contents.append(str(empty_line) * after) before, after = elt.maybe_empty_lines(current_line) dst_contents.append(str(empty_line) * before) for line in transform_line(current_line, mode=mode, features=split_line_features): dst_contents.append(str(line)) return "".join(dst_contents)
def main( # noqa: C901 ctx: click.Context, code: Optional[str], line_length: int, target_version: List[TargetVersion], check: bool, diff: bool, color: bool, fast: bool, pyi: bool, ipynb: bool, python_cell_magics: Sequence[str], skip_string_normalization: bool, skip_magic_trailing_comma: bool, experimental_string_processing: bool, preview: bool, quiet: bool, verbose: bool, required_version: Optional[str], include: Pattern[str], exclude: Optional[Pattern[str]], extend_exclude: Optional[Pattern[str]], force_exclude: Optional[Pattern[str]], stdin_filename: Optional[str], workers: int, src: Tuple[str, ...], config: Optional[str], ) -> None: """The uncompromising code formatter.""" ctx.ensure_object(dict) if src and code is not None: out( main.get_usage(ctx) + "\n\n'SRC' and 'code' cannot be passed simultaneously.") ctx.exit(1) if not src and code is None: out(main.get_usage(ctx) + "\n\nOne of 'SRC' or 'code' is required.") ctx.exit(1) root, method = find_project_root(src) if code is None else (None, None) ctx.obj["root"] = root if verbose: if root: out( f"Identified `{root}` as project root containing a {method}.", fg="blue", ) normalized = [(normalize_path_maybe_ignore(Path(source), root), source) for source in src] srcs_string = ", ".join([ f'"{_norm}"' if _norm else f'\033[31m"{source} (skipping - invalid)"\033[34m' for _norm, source in normalized ]) out(f"Sources to be formatted: {srcs_string}", fg="blue") if config: config_source = ctx.get_parameter_source("config") user_level_config = str(find_user_pyproject_toml()) if config == user_level_config: out( "Using configuration from user-level config at " f"'{user_level_config}'.", fg="blue", ) elif config_source in ( ParameterSource.DEFAULT, ParameterSource.DEFAULT_MAP, ): out("Using configuration from project root.", fg="blue") else: out(f"Using configuration in '{config}'.", fg="blue") error_msg = "Oh no! 💥 💔 💥" if (required_version and required_version != __version__ and required_version != __version__.split(".")[0]): err(f"{error_msg} The required version `{required_version}` does not match" f" the running version `{__version__}`!") ctx.exit(1) if ipynb and pyi: err("Cannot pass both `pyi` and `ipynb` flags!") ctx.exit(1) write_back = WriteBack.from_configuration(check=check, diff=diff, color=color) if target_version: versions = set(target_version) else: # We'll autodetect later. versions = set() mode = Mode( target_versions=versions, line_length=line_length, is_pyi=pyi, is_ipynb=ipynb, string_normalization=not skip_string_normalization, magic_trailing_comma=not skip_magic_trailing_comma, experimental_string_processing=experimental_string_processing, preview=preview, python_cell_magics=set(python_cell_magics), ) if code is not None: # Run in quiet mode by default with -c; the extra output isn't useful. # You can still pass -v to get verbose output. quiet = True report = Report(check=check, diff=diff, quiet=quiet, verbose=verbose) if code is not None: reformat_code(content=code, fast=fast, write_back=write_back, mode=mode, report=report) else: try: sources = get_sources( ctx=ctx, src=src, quiet=quiet, verbose=verbose, include=include, exclude=exclude, extend_exclude=extend_exclude, force_exclude=force_exclude, report=report, stdin_filename=stdin_filename, ) except GitWildMatchPatternError: ctx.exit(1) path_empty( sources, "No Python files are present to be formatted. Nothing to do 😴", quiet, verbose, ctx, ) if len(sources) == 1: reformat_one( src=sources.pop(), fast=fast, write_back=write_back, mode=mode, report=report, ) else: reformat_many( sources=sources, fast=fast, write_back=write_back, mode=mode, report=report, workers=workers, ) if verbose or not quiet: if code is None and (verbose or report.change_count or report.failure_count): out() out(error_msg if report.return_code else "All done! ✨ 🍰 ✨") if code is None: click.echo(str(report), err=True) ctx.exit(report.return_code)
def cancel(tasks: Iterable["asyncio.Task[Any]"]) -> None: """asyncio signal handler that cancels all `tasks` and reports to stderr.""" err("Aborted!") for task in tasks: task.cancel()
def failed(self, src: Path, message: str) -> None: """Increment the counter for failed reformatting. Write out a message.""" err(f"error: cannot format {src}: {message}") self.failure_count += 1