"""Settings for reformater.""" # pylint: disable=C0301,C0103 # flake8: noqa import logging import re ## get pyproject.toml settings from pathlib import Path from typing import Dict, List, Optional, Union import tomlkit import yaml from click import echo from colorama import Fore logger = logging.getLogger(__name__) def find_pyproject(src: Path) -> Optional[Path]: """Search upstream for a pyprojec.toml file.""" for directory in [src, *src.resolve().parents]: candidate = directory / "pyproject.toml" if candidate.is_file(): return candidate return None def find_djlint_rules(src: Path) -> Optional[Path]: """Search upstream for a pyprojec.toml file.""" for directory in [src, *src.resolve().parents]: candidate = directory / ".djlint_rules.yaml" if candidate.is_file(): return candidate return None def load_pyproject_settings(src: Path) -> Dict: """Load djlint config from pyproject.toml.""" djlint_content: Dict = {} pyproject_file = find_pyproject(src) if pyproject_file: content = tomlkit.parse(pyproject_file.read_text(encoding="utf8")) try: djlint_content = content["tool"]["djlint"] # type: ignore except KeyError: logger.info("No pyproject.toml found.") return djlint_content def validate_rules(rules: List) -> List: """Validate a list of linter rules. Returns valid rules.""" clean_rules = [] for rule in rules: # check for name warning = 0 name = rule["rule"].get("name", "undefined") if "name" not in rule["rule"]: warning += 1 echo(Fore.RED + "Warning: A rule is missing a name! 😢") if "patterns" not in rule["rule"]: warning += 1 echo(Fore.RED + f"Warning: Rule {name} is missing a pattern! 😢") if "message" not in rule["rule"]: warning += 1 echo(Fore.RED + f"Warning: Rule {name} is missing a message! 😢") if warning == 0: clean_rules.append(rule) return clean_rules def load_custom_rules(src: Path) -> List: """Load djlint config from pyproject.toml.""" djlint_content: List = [] djlint_rules_file = find_djlint_rules(src) if djlint_rules_file: djlint_content = yaml.load( Path(djlint_rules_file).read_text(encoding="utf8"), Loader=yaml.SafeLoader, ) return djlint_content def build_custom_blocks(custom_blocks: Union[str, None]) -> Optional[str]: """Build regex string for custom template blocks.""" if custom_blocks: return "|" + "|".join(x.strip() for x in custom_blocks.split(",")) return None class Config: """Djling Config.""" def __init__( self, src: str, ignore: Optional[str] = None, extension: Optional[str] = None, indent: Optional[int] = None, quiet: bool = False, profile: Optional[str] = None, require_pragma: bool = False, reformat: bool = False, check: bool = False, lint: bool = False, ): self.reformat = reformat self.check = check self.lint = lint self.stdin = "-" in src djlint_settings = load_pyproject_settings(Path(src)) # custom configuration options self.extension: str = str(extension or djlint_settings.get("extension", "html")) self.quiet: bool = quiet or djlint_settings.get("quiet", False) self.require_pragma: bool = ( require_pragma or str(djlint_settings.get("require_pragma", "false")).lower() == "true" ) self.custom_blocks: str = str( build_custom_blocks(djlint_settings.get("custom_blocks")) or "" ) # ignore is based on input and also profile self.ignore: str = str(ignore or djlint_settings.get("ignore", "")) # codes to exclude profile_dict: Dict[str, List[str]] = { "django": ["J", "N", "M"], "jinja": ["D", "N", "M"], "nunjucks": ["D", "J", "M"], "handlebars": ["D", "J", "N"], "golang": ["D", "J", "N", "M"], } self.profile_code: List[str] = profile_dict.get( str(profile or djlint_settings.get("profile", "all")).lower(), [] ) self.profile: str = str( profile or djlint_settings.get("profile", "all") ).lower() # load linter rules rule_set = validate_rules( yaml.load( (Path(__file__).parent / "rules.yaml").read_text(encoding="utf8"), Loader=yaml.SafeLoader, ) + load_custom_rules(Path(src)) ) self.linter_rules = list( filter( lambda x: x["rule"]["name"] not in self.ignore.split(",") and x["rule"]["name"][0] not in self.profile_code and self.profile not in x["rule"].get("exclude", []), rule_set, ) ) # base options default_indent = 4 if not indent: try: indent = int(djlint_settings.get("indent", default_indent)) except ValueError: echo( Fore.RED + f"Error: Invalid pyproject.toml indent value {djlint_settings['indent']}" ) indent = default_indent self.indent: str = indent * " " default_exclude: str = r""" \.venv | venv | \.tox | \.eggs | \.git | \.hg | \.mypy_cache | \.nox | \.svn | \.bzr | _build | buck-out | build | dist | \.pants\.d | \.direnv | node_modules | __pypackages__ """ self.exclude: str = djlint_settings.get("exclude", default_exclude) extend_exclude: str = djlint_settings.get("extend_exclude", "") if extend_exclude: self.exclude += r" | " + r" | ".join( re.escape(x.strip()) for x in extend_exclude.split(",") ) # add blank line after load tags self.blank_line_after_tag: Optional[str] = djlint_settings.get( "blank_line_after_tag", None ) # contents of tags will not be formatted self.ignored_block_opening: str = r"""