#!/usr/bin/python """ Check Django template syntax. usage:: djlint src options: -e or --extension | --check | will check html formatting for needed changes --reformat | will reformat html """ import os import re import shutil import sys import tempfile from concurrent.futures import ProcessPoolExecutor, as_completed from functools import partial from pathlib import Path from typing import List import click from click import echo from colorama import Fore, Style, deinit, init from tqdm import tqdm from .lint import lint_file from .reformat import reformat_file from .settings import Config def get_src(src: List[Path], config: Config) -> List[Path]: """Get source files.""" paths = [] for item in src: if Path.is_file(item): paths.append(item) else: # remove leading . from extension extension = str(config.extension) extension = extension[1:] if extension.startswith(".") else extension paths.extend( filter( lambda x: not re.search(config.exclude, str(x), re.VERBOSE), list(item.glob(f"**/*.{extension}")), ) ) if len(paths) == 0: echo(Fore.BLUE + "No files to check! ๐Ÿ˜ข") return [] return paths def build_output(error: dict) -> int: """Build output for file errors.""" errors = sorted(list(error.values())[0], key=lambda x: int(x["line"].split(":")[0])) width, _ = shutil.get_terminal_size() if len(errors) == 0: return 0 echo( f"{Fore.GREEN}{Style.BRIGHT}\n{list(error.keys())[0]}\n{Style.DIM}" + "".join(["โ”€" for x in range(1, width)]) + Style.RESET_ALL ) for message in errors: echo( (Fore.RED if bool(message["code"][:1] == "E") else Fore.YELLOW) + message["code"] + Style.RESET_ALL + Fore.BLUE + " " + message["line"] + Style.RESET_ALL + " " + message["message"] + Fore.BLUE + " " + message["match"], err=False, ) return len(errors) def build_check_output(errors: dict, quiet: bool) -> int: """Build output for reformat check.""" if len(errors) == 0: return 0 color = {"-": Fore.YELLOW, "+": Fore.GREEN, "@": Style.BRIGHT + Fore.BLUE} width, _ = shutil.get_terminal_size() if quiet is True and len(list(errors.values())[0]) > 0: echo( Fore.GREEN + Style.BRIGHT + str(list(errors.keys())[0]) + Style.DIM + Style.RESET_ALL ) elif quiet is False and len(list(errors.values())[0]) > 0: echo( Fore.GREEN + Style.BRIGHT + "\n" + str(list(errors.keys())[0]) + "\n" + Style.DIM + "".join(["โ”€" for x in range(1, width)]) + Style.RESET_ALL ) for diff in list(errors.values())[0][2:]: echo( f"{ color.get(diff[:1], Style.RESET_ALL)}{diff}{Style.RESET_ALL}", err=False, ) return len(list(filter(lambda x: len(x) > 0, errors.values()))) def build_quantity(size: int) -> str: """Count files in a list.""" return str(size) + " file" + ("s" if size > 1 or size == 0 else "") def build_quantity_tense(size: int) -> str: """Count files in a list.""" return ( str(size) + " file" + ("s" if size > 1 or size == 0 else "") + " " + ("were" if size > 1 or size == 0 else "was") ) @click.command(context_settings={"help_option_names": ["-h", "--help"]}) @click.argument( "src", type=click.Path( exists=True, file_okay=True, dir_okay=True, readable=True, allow_dash=True ), nargs=-1, required=True, metavar="SRC ...", ) @click.version_option(package_name="djlint") @click.option( "-e", "--extension", type=str, default="", help="File extension to lint", show_default=True, ) @click.option( "-i", "--ignore", type=str, default="", help='Codes to ignore. ex: "W013,W014"', show_default=False, ) @click.option( "--reformat", is_flag=True, help="Reformat the file(s).", ) @click.option( "--check", is_flag=True, help="Check formatting on the file(s).", ) @click.option( "--quiet", is_flag=True, help="Do not print diff when reformatting.", ) def main( src: List[str], extension: str, ignore: str, reformat: bool, check: bool, quiet: bool, ) -> None: """djLint ยท lint and reformat HTML templates.""" config = Config(src[0], extension=extension, ignore=ignore, quiet=quiet) temp_file = None if src == ["-"]: stdin_stream = click.get_text_stream("stdin") stdin_text = stdin_stream.read() temp_file = tempfile.NamedTemporaryFile(delete=False) temp_file.write(str.encode(stdin_text)) temp_file.seek(0) file_list = get_src([Path(temp_file.name)], config) else: file_list = get_src([Path(x) for x in src], config) if len(file_list) == 0: return file_quantity = build_quantity(len(file_list)) message = "Lint" if check is True: message = "Check" elif reformat is True: message = "Reformatt" # pylint: disable=C0209 bar_message = ( "{}{}{} {}{{n_fmt}}/{{total_fmt}}{} {}files{} {{bar}} {}{{elapsed}}{}".format( Fore.BLUE + Style.BRIGHT, message + "ing", Style.RESET_ALL, Fore.RED + Style.BRIGHT, Style.RESET_ALL, Fore.BLUE + Style.BRIGHT, Style.RESET_ALL, Fore.GREEN + Style.BRIGHT, Style.RESET_ALL + " ", ) ) echo() worker_count = os.cpu_count() if sys.platform == "win32": # Work around https://bugs.python.org/issue26903 worker_count = min(worker_count, 60) with ProcessPoolExecutor(max_workers=worker_count) as exe: file_errors = [] if reformat is True or check is True: func = partial(reformat_file, config, check) futures = { exe.submit(func, this_file): this_file for this_file in file_list } else: func = partial(lint_file, config) futures = { exe.submit(func, this_file): this_file for this_file in file_list } elapsed = "00:00" with tqdm( total=len(file_list), bar_format=bar_message, colour="BLUE", ascii="โ”ˆโ”", leave=False, ) as pbar: for future in as_completed(futures): futures[future] file_errors.append(future.result()) pbar.update() elapsed = pbar.format_interval(pbar.format_dict["elapsed"]) finshed_bar_message = ( "{}{}{} {}{{n_fmt}}/{{total_fmt}}{} {}files{} {{bar}} {}{}{} ".format( Fore.BLUE + Style.BRIGHT, message + "ing", Style.RESET_ALL, Fore.GREEN + Style.BRIGHT, Style.RESET_ALL, Fore.BLUE + Style.BRIGHT, Style.RESET_ALL, Fore.GREEN + Style.BRIGHT, elapsed, Style.RESET_ALL, ) ) finished_bar = tqdm( total=len(file_list), initial=len(file_list), bar_format=finshed_bar_message, colour="GREEN", ascii="โ”ˆโ”", leave=True, ) finished_bar.close() # format errors success_message = "" error_count = 0 echo() if reformat is True or check is True: # reformat message for error in file_errors: error_count += build_check_output(error, quiet) tense_message = ( build_quantity(error_count) + " would be" if check is True else build_quantity_tense(error_count) ) success_message = f"{tense_message} updated." else: # lint message for error in file_errors: error_count += build_output(error) error_case = "error" if error_count == 1 else "errors" success_message = ( f"{message}ed {file_quantity}, found {error_count} {error_case}." ) success_color = Fore.RED + Style.BRIGHT if error_count > 0 else Fore.BLUE echo(f"\n{success_color}{success_message}{Style.RESET_ALL}\n") if temp_file: temp_file.close() os.unlink(temp_file.name) if bool(error_count): sys.exit(1) if __name__ == "__main__": init(autoreset=True) main() deinit()