from __future__ import annotations from pathlib import Path from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple, Union, cast from redbot import VersionInfo, version_info as red_version_info from . import installable from .log import log if TYPE_CHECKING: from .json_mixins import RepoJSONMixin __all__ = ("REPO_SCHEMA", "INSTALLABLE_SCHEMA", "update_mixin") class UseDefault: """To be used as sentinel.""" # sentinel value USE_DEFAULT = UseDefault() def ensure_tuple_of_str( info_file: Path, key_name: str, value: Union[Any, UseDefault] ) -> Tuple[str, ...]: default: Tuple[str, ...] = () if value is USE_DEFAULT: return default if not isinstance(value, list): log.warning( "Invalid value of '%s' key (expected list, got %s)" " in JSON information file at path: %s", key_name, type(value).__name__, info_file, ) return default for item in value: if not isinstance(item, str): log.warning( "Invalid item in '%s' list (expected str, got %s)" " in JSON information file at path: %s", key_name, type(item).__name__, info_file, ) return default return tuple(value) def ensure_str(info_file: Path, key_name: str, value: Union[Any, UseDefault]) -> str: default = "" if value is USE_DEFAULT: return default if not isinstance(value, str): log.warning( "Invalid value of '%s' key (expected str, got %s)" " in JSON information file at path: %s", key_name, type(value).__name__, info_file, ) return default return value def ensure_red_version_info( info_file: Path, key_name: str, value: Union[Any, UseDefault] ) -> VersionInfo: default = red_version_info if value is USE_DEFAULT: return default if not isinstance(value, str): log.warning( "Invalid value of '%s' key (expected str, got %s)" " in JSON information file at path: %s", key_name, type(value).__name__, info_file, ) return default try: version_info = VersionInfo.from_str(value) except ValueError: log.warning( "Invalid value of '%s' key (given value isn't a valid version string)" " in JSON information file at path: %s", key_name, info_file, ) return default return version_info def ensure_python_version_info( info_file: Path, key_name: str, value: Union[Any, UseDefault] ) -> Tuple[int, int, int]: default = (3, 5, 1) if value is USE_DEFAULT: return default if not isinstance(value, list): log.warning( "Invalid value of '%s' key (expected list, got %s)" " in JSON information file at path: %s", key_name, type(value).__name__, info_file, ) return default count = len(value) if count != 3: log.warning( "Invalid value of '%s' key (expected list with 3 items, got %s items)" " in JSON information file at path: %s", key_name, count, info_file, ) return default for item in value: if not isinstance(item, int): log.warning( "Invalid item in '%s' list (expected int, got %s)" " in JSON information file at path: %s", key_name, type(item).__name__, info_file, ) return default return cast(Tuple[int, int, int], tuple(value)) def ensure_bool( info_file: Path, key_name: str, value: Union[Any, UseDefault], *, default: bool = False ) -> bool: if value is USE_DEFAULT: return default if not isinstance(value, bool): log.warning( "Invalid value of '%s' key (expected bool, got %s)" " in JSON information file at path: %s", key_name, type(value).__name__, info_file, ) return default return value def ensure_required_cogs_mapping( info_file: Path, key_name: str, value: Union[Any, UseDefault] ) -> Dict[str, str]: default: Dict[str, str] = {} if value is USE_DEFAULT: return default if not isinstance(value, dict): log.warning( "Invalid value of '%s' key (expected dict, got %s)" " in JSON information file at path: %s", key_name, type(value).__name__, info_file, ) return default # keys in json dicts are always strings for item in value.values(): if not isinstance(item, str): log.warning( "Invalid item in '%s' dict (expected str, got %s)" " in JSON information file at path: %s", key_name, type(item).__name__, info_file, ) return default return value def ensure_installable_type( info_file: Path, key_name: str, value: Union[Any, UseDefault] ) -> installable.InstallableType: default = installable.InstallableType.COG if value is USE_DEFAULT: return default if not isinstance(value, str): log.warning( "Invalid value of '%s' key (expected str, got %s)" " in JSON information file at path: %s", key_name, type(value).__name__, info_file, ) return default # NOTE: old behavior was to use InstallableType.UNKNOWN if value in ("", "COG"): return installable.InstallableType.COG if value == "SHARED_LIBRARY": return installable.InstallableType.SHARED_LIBRARY return installable.InstallableType.UNKNOWN EnsureCallable = Callable[[Path, str, Union[Any, UseDefault]], Any] SchemaType = Dict[str, EnsureCallable] REPO_SCHEMA: SchemaType = { "author": ensure_tuple_of_str, "description": ensure_str, "install_msg": ensure_str, "short": ensure_str, } INSTALLABLE_SCHEMA: SchemaType = { "min_bot_version": ensure_red_version_info, "max_bot_version": ensure_red_version_info, "min_python_version": ensure_python_version_info, "hidden": ensure_bool, "disabled": ensure_bool, "required_cogs": ensure_required_cogs_mapping, "requirements": ensure_tuple_of_str, "tags": ensure_tuple_of_str, "type": ensure_installable_type, "end_user_data_statement": ensure_str, } def update_mixin(repo_or_installable: RepoJSONMixin, schema: SchemaType) -> None: info = repo_or_installable._info info_file = repo_or_installable._info_file for key, callback in schema.items(): setattr(repo_or_installable, key, callback(info_file, key, info.get(key, USE_DEFAULT)))