From 6ceb23b84beedd861c764498995b207625c4c771 Mon Sep 17 00:00:00 2001 From: Antonio Cordero Balcazar Date: Fri, 3 Apr 2026 15:53:10 +0200 Subject: [PATCH] * Moved known models and their detection to the configuration file. * Updated `ppp_config.yaml.defaults` to enhance documentation and structure for model detection and configuration. * Improved validation of configuration files. * Added a new `tests` application in `SUPPORTED_APPS` for testing purposes. * Support for variables in list of values comparisons. --- .vscode/settings.json | 2 +- docs/CONFIG.md | 6 +- grammar.lark | 5 +- ppp.py | 408 +++++++++++++++++++++++++++++++-------- ppp_comfyui.py | 56 +----- ppp_config.yaml.defaults | 214 ++++++++++++++++++-- ppp_enmappings.py | 2 +- ppp_hosts.py | 2 + ppp_wildcards.py | 2 +- scripts/ppp_script.py | 46 +---- tests/tests.py | 37 +++- 11 files changed, 567 insertions(+), 213 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index d52f55c..b8a6993 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,7 +3,7 @@ "python.testing.unittestArgs": [ "-v", "-s", - "./tests", + ".", "-p", "test*.py" ], diff --git a/docs/CONFIG.md b/docs/CONFIG.md index a804dd5..94882ba 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -4,11 +4,11 @@ The extension supports a configuration file `ppp_config.yaml` with some settings that don't usually change. -By default this configuration is read from the file `ppp_config.yaml.defaults` in the extension folder (that file must not be modified). If you want to personalize any settings you should first copy this file as `ppp_config.yaml` in the same folder, or in the ComfyUI user folder (preferred, but only possible in ComfyUI). The options in this file will take precedence over those in the defaults file. +By default this configuration is read from the file `ppp_config.yaml.defaults` in the extension folder. That file must not be modified. If you want to personalize any settings you should first copy it as `ppp_config.yaml` in the same folder, or in the ComfyUI user folder (preferred, but only possible in ComfyUI). The options in this new file will take precedence over those in the defaults file. -This file contains some options for how the host applications (WebUIs) should act in certain operations, and also options for the supported models, which currently include only the model variants definitions. Hosts and models names are fixed values (those supported by the extension). The defaults file contains comments to explain the available options. +This file contains some options for how the host applications (WebUIs) should act in certain operations, and also define the supported models, including how to detect them and the model variants definitions. Host names are fixed values (those supported by the extension). The defaults file contains comments to explain the available options. -The model variants now support regular expressions instead of a list of strings to detect the variant. If you used a non default value in previous versions you should re-do it in this file with the new format. As before, the default defines variants for *Pony* and *Illustrious* models. +The model variants now support regular expressions instead of a list of strings to detect the variant. If you used a non default value in previous versions you should create a configuration file and add them with the new format. As before, the default file defines variants for *Pony* and *Illustrious* models. ## ComfyUI diff --git a/grammar.lark b/grammar.lark index 1bb0961..b04a622 100644 --- a/grammar.lark +++ b/grammar.lark @@ -8,6 +8,7 @@ WC_NAME_PLAIN: /(?:(?!__|\$\$|\$\{|${]|\\.)+/s // exclude only the starting ones @@ -112,9 +113,9 @@ operation_and: condition _WHITESPACE "and" _WHITESPACE condition operation_or: condition _WHITESPACE "or" _WHITESPACE condition operation_not: "not" ( ( _WHITESPACE ungrouped_condition ) | ( _WHITESPACE? grouped_condition ) ) truthy_operand: IDENTIFIER -comparison_simple_value: IDENTIFIER _WHITESPACE ( /not/ _WHITESPACE )? /eq|ne|gt|lt|ge|le|contains/ _WHITESPACE ( SIMPLEVALUE | IDENTIFIER ) +comparison_simple_value: IDENTIFIER _WHITESPACE ( /not/ _WHITESPACE )? /eq|ne|gt|lt|ge|le|contains/ _WHITESPACE COMPVALUE comparison_list_value: IDENTIFIER _WHITESPACE ( /not/ _WHITESPACE )? /contains|in/ _WHITESPACE listvalue -listvalue.9: "(" _WHITESPACE? SIMPLEVALUE ( _WHITESPACE? "," _WHITESPACE? SIMPLEVALUE )* _WHITESPACE? ")" +listvalue.9: "(" _WHITESPACE? COMPVALUE ( _WHITESPACE? "," _WHITESPACE? COMPVALUE )* _WHITESPACE? ")" // command: set commandset: "" commandsetcontent "" diff --git a/ppp.py b/ppp.py index 4faa603..63109f1 100644 --- a/ppp.py +++ b/ppp.py @@ -96,33 +96,6 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in UNPROCESSED_STOP = "UNPROCESSED CONSTRUCTS!\nBREAK " INVALID_CONTENT_STOP = "INVALID CONTENT! {0}\nBREAK " - KNOWN_MODELS = [ - "sd1", # Stable Diffusion 1 - "sd2", # Stable Diffusion 2 - "ssd", # Segmind Stable Diffusion 1B - "sdxl", # Stable Diffusion XL - "sd3", # Stable Diffusion 3 - "flux", # Flux 1 - "auraflow", # AuraFlow - "pixart", # PixArt - "lumina2", # Lumina2 & ZImage - "ltxv", # LTXV - "cosmos", # Cosmos - "cosmospredict2", # CosmosPredict2 - "genmomochi", # GenmoMochi - "hunyuan", # Hunyuan - "hunyuanvideo", # HunyuanVideo - "hunyuan3d", # Hunyuan3D - "wan", # Wan21 & Wan22 - "hidream", # HiDream - "qwenimage", # QwenImage & QwenImageEdit - "chroma", # Chroma - "omnigen2", # Omnigen 2 - "flux2", # Flux 2 - "kandinsky5", # Kandinsky 5 - "anima", # Anima - ] - def __init__( self, logger: logging.Logger, @@ -152,14 +125,28 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in self.env_info = env_info self.wildcard_obj = wildcards_obj self.extranetwork_mappings_obj = extranetwork_mappings_obj + default_config_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ppp_config.yaml.defaults") - with open(default_config_file, "r", encoding="utf-8") as f: - self.config: dict[str, Any] = yaml.safe_load(f) + try: + with open(default_config_file, "r", encoding="utf-8") as f: + self.config: dict[str, Any] = yaml.safe_load(f) + except Exception as exc: # pylint: disable=broad-exception-caught + self.config = {} + raise PPPInterrupt(f"Failed to load default configuration from '{default_config_file}'.") from exc + validate_def_cfg = self.__validate_normalize_configuration(self.config, "default configuration file") + if validate_def_cfg != 0: + errmsg = "Default configuration file has errors. Please restore the default configuration file and, per instructions, use a copy to adapt it." + if validate_def_cfg == 2: + raise PPPInterrupt(errmsg) + else: + self.logger.warning(errmsg) + user_config_file = self.env_info.get("ppp_config", "") + user_config: dict[str, Any] = {} if isinstance(user_config_file, dict): user_config = user_config_file + self.__validate_normalize_configuration(user_config, "forced configuration") else: - user_config = {} if user_config_file == "": if self.env_info.get("app", "") == SUPPORTED_APPS.comfyui.value: try: @@ -169,70 +156,102 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in if user_dir and os.path.isdir(user_dir): user_config_file = os.path.join(user_dir, "default", "ppp_config.yaml") except Exception: # pylint: disable=broad-exception-caught - pass + self.logger.warning("Failed to get user directory for PPP config.") if not user_config_file or not os.path.exists(user_config_file): user_config_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ppp_config.yaml") if user_config_file and os.path.exists(user_config_file): - with open(user_config_file, "r", encoding="utf-8") as f: user_config = yaml.safe_load(f) - # Merge user config into default config - if len(user_config): - for k in ("hosts", "models"): - if k in user_config: - for key, value in user_config[k].items(): - if key in self.config[k] and isinstance(self.config[k][key], list): - self.config[k][key].extend(value) - else: - self.config[k][key] = value - self.host_config: dict[str, Any] = (self.config.get("hosts") or {}).get(self.env_info.get("app", "")) or {} + self.__validate_normalize_configuration(user_config, "user configuration") + self.__merge_configuration(user_config) + + self.models_config: dict[str, dict[str, Any] | None] = self.config.get("models") or {} + self.known_models: list[str] = list(self.models_config.keys()) + + # Patch for tests (copy comfyui) + if self.env_info.get("app", "") == "tests": + self.config.setdefault("hosts", {}).setdefault("tests", {}) + for m in self.known_models: + self.models_config.setdefault(m, {}).setdefault("detect", {})["tests"] = ( + self.models_config[m].get("detect", {}).get("comfyui", None) + ) + + self.host_config: dict[str, Any] = (self.config.get("hosts") or {}).get(self.env_info.get("app", "")) + if self.host_config is None: + raise PPPInterrupt( + f"No host configuration found for app '{self.env_info.get('app', '')}'. Please check your configuration." + ) + + # Update env_info with model detection + prop_base = self.env_info.get("property_base", None) + model_class = self.env_info.get("model_class", "") + for m in self.known_models: + self.env_info["is_" + m] = False + model_config = (self.models_config.get(m) or {}).get("detect", {}).get(self.env_info.get("app", ""), {}) + if model_config is not None: + cls_list = model_config.get("class", []) + if model_class in cls_list: + self.env_info["is_" + m] = True + elif "property" in model_config and prop_base is not None: + prop = model_config["property"] + attr = getattr(prop_base, prop, None) + if isinstance(attr, bool) and attr: + self.env_info["is_" + m] = True + # General options - self.debug_level = DEBUG_LEVEL(options.get("debug_level", self.DEFAULT_DEBUG_LEVEL)) - self.gen_onwarning = self.ONWARNING_CHOICES(options.get("on_warning", self.DEFAULT_ONWARNING)) - self.variants_definitions = { - v: (m, [vo["find_in_filename"]] if isinstance(vo["find_in_filename"], str) else vo["find_in_filename"]) - for m in self.KNOWN_MODELS - for v, vo in (((self.config.get("models") or {}).get(m) or {}).get("variants") or {}).items() - } + self.debug_level = DEBUG_LEVEL(self.options.get("debug_level", self.DEFAULT_DEBUG_LEVEL)) + self.gen_onwarning = self.ONWARNING_CHOICES(self.options.get("on_warning", self.DEFAULT_ONWARNING)) + self.variants_definitions = {} + for m in self.known_models: + for v, vo in (((self.models_config or {}).get(m) or {}).get("variants") or {}).items(): + if v not in self.known_models: + self.variants_definitions[v] = (m, vo["find_in_filename"]) + else: + self.logger.warning( + f"Variant name '{v}' in model '{m}' conflicts with a known model name. Discarding variant." + ) + if self.debug_level != DEBUG_LEVEL.none: self.logger.debug(self.format_output(f"Host configuration: {self.host_config}")) # Wildcards options - self.wil_process_wildcards = options.get("process_wildcards", self.DEFAULT_WC_PROCESS) - self.wil_keep_choices_order = options.get("keep_choices_order", self.DEFAULT_KEEP_CHOICES_ORDER) - self.wil_choice_separator = options.get("choice_separator", self.DEFAULT_CHOICE_SEPARATOR) - self.wil_ifwildcards = self.IFWILDCARDS_CHOICES(options.get("if_wildcards", self.DEFAULT_IF_WILDCARDS)) + self.wil_process_wildcards = self.options.get("process_wildcards", self.DEFAULT_WC_PROCESS) + self.wil_keep_choices_order = self.options.get("keep_choices_order", self.DEFAULT_KEEP_CHOICES_ORDER) + self.wil_choice_separator = self.options.get("choice_separator", self.DEFAULT_CHOICE_SEPARATOR) + self.wil_ifwildcards = self.IFWILDCARDS_CHOICES(self.options.get("if_wildcards", self.DEFAULT_IF_WILDCARDS)) # Send to negative options - self.stn_ignore_repeats = options.get("stn_ignore_repeats", self.DEFAULT_STN_IGNORE_REPEATS) - self.stn_separator = options.get("stn_separator", self.DEFAULT_STN_SEPARATOR) + self.stn_ignore_repeats = self.options.get("stn_ignore_repeats", self.DEFAULT_STN_IGNORE_REPEATS) + self.stn_separator = self.options.get("stn_separator", self.DEFAULT_STN_SEPARATOR) # Cleanup and remove options - self.cup_do_cleanup = options.get("do_cleanup", self.DEFAULT_DO_CLEANUP) - self.cup_cleanup_variables = options.get("cleanup_variables", self.DEFAULT_CLEANUP_VARIABLES) - self.cup_extraspaces = self.cup_do_cleanup and options.get( + self.cup_do_cleanup = self.options.get("do_cleanup", self.DEFAULT_DO_CLEANUP) + self.cup_cleanup_variables = self.options.get("cleanup_variables", self.DEFAULT_CLEANUP_VARIABLES) + self.cup_extraspaces = self.cup_do_cleanup and self.options.get( "cleanup_extra_spaces", self.DEFAULT_CUP_EXTRA_SPACES ) - self.cup_emptyconstructs = self.cup_do_cleanup and options.get( + self.cup_emptyconstructs = self.cup_do_cleanup and self.options.get( "cleanup_empty_constructs", self.DEFAULT_CUP_EMPTY_CONSTRUCTS ) - self.cup_extraseparators = self.cup_do_cleanup and options.get( + self.cup_extraseparators = self.cup_do_cleanup and self.options.get( "cleanup_extra_separators", self.DEFAULT_CUP_EXTRA_SEPARATORS ) - self.cup_extraseparators2 = self.cup_do_cleanup and options.get( + self.cup_extraseparators2 = self.cup_do_cleanup and self.options.get( "cleanup_extra_separators2", self.DEFAULT_CUP_EXTRA_SEPARATORS2 ) - self.cup_extraseparators_include_eol = self.cup_do_cleanup and options.get( + self.cup_extraseparators_include_eol = self.cup_do_cleanup and self.options.get( "cleanup_extra_separators_include_eol", self.DEFAULT_CUP_EXTRA_SEPARATORS_INCLUDE_EOL ) - self.cup_breaks = self.cup_do_cleanup and options.get("cleanup_breaks", self.DEFAULT_CUP_BREAKS) - self.cup_breaks_eol = self.cup_do_cleanup and options.get("cleanup_breaks_eol", self.DEFAULT_CUP_BREAKS_EOL) - self.cup_ands = self.cup_do_cleanup and options.get("cleanup_ands", self.DEFAULT_CUP_ANDS) - self.cup_ands_eol = self.cup_do_cleanup and options.get("cleanup_ands_eol", self.DEFAULT_CUP_ANDS_EOL) - self.cup_extranetworktags = self.cup_do_cleanup and options.get( + self.cup_breaks = self.cup_do_cleanup and self.options.get("cleanup_breaks", self.DEFAULT_CUP_BREAKS) + self.cup_breaks_eol = self.cup_do_cleanup and self.options.get( + "cleanup_breaks_eol", self.DEFAULT_CUP_BREAKS_EOL + ) + self.cup_ands = self.cup_do_cleanup and self.options.get("cleanup_ands", self.DEFAULT_CUP_ANDS) + self.cup_ands_eol = self.cup_do_cleanup and self.options.get("cleanup_ands_eol", self.DEFAULT_CUP_ANDS_EOL) + self.cup_extranetworktags = self.cup_do_cleanup and self.options.get( "cleanup_extranetwork_tags", self.DEFAULT_CUP_EXTRANETWORK_TAGS ) - self.cup_mergeattention = self.cup_do_cleanup and options.get( + self.cup_mergeattention = self.cup_do_cleanup and self.options.get( "cleanup_merge_attention", self.DEFAULT_CUP_MERGE_ATTENTION ) - self.rem_removeextranetworktags = self.cup_do_cleanup and options.get( + self.rem_removeextranetworktags = self.cup_do_cleanup and self.options.get( "remove_extranetwork_tags", self.DEFAULT_CUP_REMOVE_EXTRANETWORK_TAGS ) @@ -374,6 +393,228 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in self.user_variables = {} self.echoed_variables = {} + def __merge_configuration(self, user_config): + """ + Merges the user configuration into the default configuration. + + Args: + user_config: The user configuration dictionary to merge. + """ + if "hosts" in user_config: + # Options are replaced by host + for host_key, host_value in user_config["hosts"].items(): + self.config["hosts"].setdefault(host_key, {}).update(host_value) + if "models" in user_config: + for model_key, model_value in user_config["models"].items(): + # We independently update the detection and variants for each model in the user configuration + usr_mdetect = model_value.get("detect", "") + # Detections are replaced by host + cfg_detect = self.config["models"].setdefault(model_key, {}).get("detect") + if cfg_detect is not None: + for host, hdetect in usr_mdetect.items(): + cfg_detect[host] = hdetect + # Variants are fully replaced if specified + usr_mvariants = model_value.get("variants", "") + if usr_mvariants is None: + del self.config["models"].setdefault(model_key, {})["variants"] + else: + self.config["models"].setdefault(model_key, {})["variants"] = usr_mvariants + + def __re_flags_from_list(self, flags_list: list[str]) -> int: + flag_value = 0 + for flag in flags_list: + if hasattr(re, flag): + flag_value |= getattr(re, flag) + else: + return 0 + return flag_value + + def __validate_find_in_filename_element( + self, + where: str, + model_key: str, + variant_key: str, + find_in_filename: str | dict, + ) -> dict | None: + if isinstance(find_in_filename, str): + try: + re.compile(find_in_filename, re.IGNORECASE) + return {"regex": find_in_filename, "flags": re.IGNORECASE} + except re.error: + self.logger.warning( + f"{where.title()}: Invalid regex pattern for variant '{variant_key}' in model '{model_key}'. Discarding variant." + ) + elif isinstance(find_in_filename, dict): + regex = find_in_filename.get("regex", "") + flags = find_in_filename.get("flags", []) + if not isinstance(regex, str) or not isinstance(flags, list) or not all(isinstance(f, str) for f in flags): + self.logger.warning( + f"{where.title()}: Invalid format for 'find_in_filename' for variant '{variant_key}' in model '{model_key}'. Discarding variant." + ) + else: + fl = self.__re_flags_from_list(flags) + if fl == 0 and len(flags): + self.logger.warning( + f"{where.title()}: Invalid regex flags for variant '{variant_key}' in model '{model_key}'. Discarding variant." + ) + try: + re.compile(regex, fl) + return {"regex": regex, "flags": fl} + except re.error: + self.logger.warning( + f"{where.title()}: Invalid regex pattern for variant '{variant_key}' in model '{model_key}'. Discarding variant." + ) + return None + + def __validate_normalize_configuration(self, cfg: dict[str, Any] | Any, where: str) -> int: + """ + Validates the configuration dictionary and normalizes it. + + Args: + cfg (dict): The configuration dictionary to validate. + + Returns: + int: 0 if the configuration is valid, 1 if there are warnings, 2 if there are fatal errors. + """ + fatal_errors = False + if not isinstance(cfg, dict): + self.logger.error(f"{where.capitalize()}: is not a dictionary.") + fatal_errors = True + else: + if cfg.get("hosts") and not isinstance(cfg["hosts"], dict): + self.logger.error(f"{where.capitalize()}: 'hosts' is not a valid dictionary.") + fatal_errors = True + if cfg.get("models") and not isinstance(cfg.get("models"), dict): + self.logger.error(f"{where.capitalize()}: 'models' is not a valid dictionary.") + fatal_errors = True + if fatal_errors: + return 2 + result = 0 + defcfg_hosts: dict[str, Any] = cfg.get("hosts", {}) + for host_key, host_value in dict(defcfg_hosts).items(): + if host_key not in SUPPORTED_APPS._value2member_map_: # pylint: disable=protected-access + self.logger.warning(f"{where.capitalize()}: Unsupported host '{host_key}'. Discarding host.") + defcfg_hosts.pop(host_key, None) + result = 1 + elif host_value is not None and ( + not isinstance(host_value, dict) + or not all(k in ["attention", "scheduling", "alternation", "and", "break"] for k in host_value) + ): + self.logger.warning(f"{where.capitalize()}: Invalid format for host '{host_key}'. Discarding host.") + defcfg_hosts.pop(host_key, None) + result = 1 + defcfg_models: dict[str, Any] = cfg.get("models", {}) + for model_key, model_value in dict(defcfg_models).items(): + if ( + not isinstance(model_value, dict) + or model_value.get("detect") is None + or not isinstance(model_value["detect"], dict) + ): + self.logger.warning(f"{where.capitalize()}: Invalid format for model '{model_key}'. Discarding model.") + defcfg_models.pop(model_key, None) + result = 1 + else: + defcfg_m_detect: dict[str, Any] = model_value["detect"] + for host_key, host_value in dict(defcfg_m_detect).items(): + if host_key not in SUPPORTED_APPS._value2member_map_: # pylint: disable=protected-access + self.logger.warning( + f"{where.capitalize()}: Unsupported host '{host_key}' in 'detect' for model '{model_key}'. Discarding host." + ) + defcfg_m_detect.pop(host_key, None) + result = 1 + elif host_value is not None: + if not isinstance(host_value, dict): + self.logger.warning( + f"{where.capitalize()}: Invalid format for host '{host_key}' in 'detect' for model '{model_key}'. Discarding host." + ) + defcfg_m_detect.pop(host_key, None) + result = 1 + elif "class" in host_value: + if not isinstance(host_value["class"], list) or not all( + isinstance(c, str) for c in host_value["class"] + ): + self.logger.warning( + f"{where.capitalize()}: Invalid format for 'class' in host '{host_key}' in 'detect' for model '{model_key}'. Discarding host." + ) + defcfg_m_detect.pop(host_key, None) + result = 1 + elif "property" in host_value: + if not isinstance(host_value["property"], str): + self.logger.warning( + f"{where.capitalize()}: Invalid format for 'property' in host '{host_key}' in 'detect' for model '{model_key}'. Discarding host." + ) + defcfg_m_detect.pop(host_key, None) + result = 1 + else: + self.logger.warning( + f"{where.capitalize()}: Neither 'class' nor 'property' specified for host '{host_key}' in 'detect' for model '{model_key}'. Discarding host." + ) + defcfg_m_detect.pop(host_key, None) + result = 1 + if "variants" in model_value: + if not isinstance(model_value["variants"], dict): + self.logger.warning( + f"{where.capitalize()}: Invalid format for 'variants' in model '{model_key}'. Discarding model." + ) + defcfg_models.pop(model_key, None) + result = 1 + else: + defcfg_m_variants: dict[str, Any] = model_value["variants"] + for variant_key, variant_value in dict(defcfg_m_variants).items(): + if not isinstance(variant_key, str) or not variant_key.isidentifier(): + self.logger.warning( + f"{where.capitalize()}: Invalid variant name '{variant_key}' in model '{model_key}'. Discarding variant." + ) + defcfg_m_variants.pop(variant_key, None) + result = 1 + elif not isinstance(variant_value, dict) or not isinstance( + variant_value.get("find_in_filename"), (str, dict, list) + ): + self.logger.warning( + f"{where.capitalize()}: Invalid format for variant '{variant_key}' in model '{model_key}'. Discarding variant." + ) + defcfg_m_variants.pop(variant_key, None) + result = 1 + else: + if isinstance(variant_value["find_in_filename"], list): + normalized_list = [] + for elem in variant_value["find_in_filename"]: + validated_elem = self.__validate_find_in_filename_element( + where, model_key, variant_key, elem + ) + if validated_elem is not None: + normalized_list.append(validated_elem) + if len(normalized_list) != len(variant_value["find_in_filename"]): + defcfg_m_variants.pop(variant_key, None) + result = 1 + else: + variant_value["find_in_filename"] = normalized_list + else: + variant_value["find_in_filename"] = [ + self.__validate_find_in_filename_element( + where, model_key, variant_key, variant_value["find_in_filename"] + ) + ] + return result + + def envinfo_hash(self) -> str: + """ + Generates a hash string based on the environment information. + + Returns: + str: A hash string representing the environment information. + """ + return hash(tuple(sorted(self.env_info.items()))) + + def options_hash(self) -> str: + """ + Generates a hash string based on the options. + + Returns: + str: A hash string representing the options. + """ + return hash(tuple(sorted(self.options.items()))) + def __preprocess_grammar(self, grammar_content: str, options: dict[str, bool]) -> str: """ Preprocesses the grammar content to handle conditional compilation directives. @@ -476,7 +717,7 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in Initializes the system variables. """ self.system_variables = {} - sdchecks = {x: self.env_info.get("is_" + x, False) for x in self.KNOWN_MODELS} + sdchecks = {x: self.env_info.get("is_" + x, False) for x in self.known_models} sdchecks.update({"": True}) self.system_variables["_model"] = next((k for k, v in sdchecks.items() if v), "") self.system_variables["_sd"] = self.system_variables["_model"] # deprecated @@ -486,12 +727,15 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in self.system_variables["_sdname"] = os.path.basename(model_filename) # deprecated self.system_variables["_modelname"] = os.path.basename(model_filename) self.system_variables["_modelclass"] = self.env_info.get("model_class", "") - is_models = { - model_name: (model_type_and_substrings[0] == "" or sdchecks.get(model_type_and_substrings[0], False)) - and any((re.search(s, model_filename, re.IGNORECASE) is not None) for s in model_type_and_substrings[1]) - for model_name, model_type_and_substrings in self.variants_definitions.items() - if model_name not in self.KNOWN_MODELS - } + is_models = {} + for model_name, model_type_and_substrings in self.variants_definitions.items(): + if not (model_type_and_substrings[0] == "" or sdchecks.get(model_type_and_substrings[0], False)): + is_models[model_name] = False + else: + is_models[model_name] = any( + (re.search(dre["regex"], model_filename, dre["flags"]) is not None) + for dre in model_type_and_substrings[1] + ) is_models_true = [k for k, v in is_models.items() if v] if len(is_models_true) > 1: self.logger.warning( @@ -1184,7 +1428,7 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in try: return int(c) except ValueError: - # Bare identifier — resolve as variable reference + # Bare identifier - resolve as variable reference if c.startswith("_"): val = self.__ppp.system_variables.get(c, None) if val is None: @@ -1248,7 +1492,11 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in ( c[1:-1].lower() if c.startswith('"') or c.startswith("'") - else True if c.lower() == "true" else False if c.lower() == "false" or c == "" else self.__resolve_cond_value(c) + else ( + True + if c.lower() == "true" + else False if c.lower() == "false" or c == "" else self.__resolve_cond_value(c) + ) ) for c in cond_value ) @@ -1264,9 +1512,7 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in try: var_value_adjusted = int(var_value) except (ValueError, TypeError): - self.warn_or_stop( - f"Cannot convert variable value '{var_value}' to integer for comparison" - ) + self.warn_or_stop(f"Cannot convert variable value '{var_value}' to integer for comparison") return False result = comp_ops[cond_comp](var_value_adjusted, c) if result: diff --git a/ppp_comfyui.py b/ppp_comfyui.py index 3ffd9f2..a117aaa 100644 --- a/ppp_comfyui.py +++ b/ppp_comfyui.py @@ -218,62 +218,8 @@ class PromptPostProcessorComfyUINode: "models_path": folder_paths.models_dir, "model_filename": modelname or "", # path is relative to checkpoints folder "model_class": modelclass, + "property_base": None, } - env_info.update({"is_" + k: False for k in PromptPostProcessor.KNOWN_MODELS}) - env_info.update( - { - "is_sd1": modelclass in ("SD15", "SD15_instructpix2pix"), - "is_sd2": modelclass in ("SD20", "SD21UnclipL", "SD21UnclipH", "LotusD"), - "is_sdxl": ( - modelclass - in ("SDXL", "SDXLRefiner", "SDXL_instructpix2pix", "Segmind_Vega", "KOALA_700M", "KOALA_1B") - ), - "is_ssd": modelclass in ("SSD1B",), - "is_sd3": modelclass in ("SD3",), - "is_flux": modelclass in ("Flux", "FluxInpaint", "FluxSchnell"), - "is_auraflow": modelclass in ("AuraFlow",), - "is_pixart": modelclass in ("PixArtAlpha", "PixArtSigma"), - "is_lumina2": modelclass in ("Lumina2", "ZImage"), - "is_ltxv": modelclass in ("LTXV", "LTXAV"), - "is_cosmos": modelclass in ("CosmosT2V", "CosmosI2V"), - "is_cosmospredict2": modelclass in ("CosmosT2IPredict2", "CosmosI2VPredict2"), - "is_genmomochi": modelclass in ("GenmoMochi",), - "is_hunyuan": modelclass in ("HunyuanDiT", "HunyuanDiT1"), - "is_hunyuanvideo": modelclass - in ( - "HunyuanVideo", - "HunyuanVideoI2V", - "HunyuanVideoSkyreelsI2V", - "HunyuanImage21", - "HunyuanImage21Refiner", - "HunyuanVideo15", - "HunyuanVideo15_SR_Distilled", - ), - "is_hunyuan3d": modelclass in ("Hunyuan3Dv2", "Hunyuan3Dv2_1", "Hunyuan3Dv2mini"), - "is_wan": modelclass - in ( - "WAN21_T2V", - "WAN21_I2V", - "WAN21_FunControl2V", - "WAN21_Camera", - "WAN22_Camera", - "WAN21_Vace", - "WAN21_HuMo", - "WAN22_S2V", - "WAN22_Animate", - "WAN22_T2V", - ), - "is_hidream": modelclass in ("HiDream",), - "is_chroma": modelclass in ("Chroma", "ChromaRadiance"), - "is_omnigen2": modelclass in ("Omnigen2",), - "is_kandinsky5": modelclass in ("Kandinsky5", "Kandinsky5Image"), - "is_qwenimage": modelclass in ("QwenImage",), - "is_flux2": modelclass in ("Flux2"), - "is_anima": modelclass in ("Anima",), - } - ) - # Also supported: SVD_img2vid, SVD3D_u, SVD3_p, Stable_Zero123, SD_X4Upscaler, Stable_Cascade_C, Stable_Cascade_B, StableAudio - wc_wildcards_folders = wc_options["wc_wildcards_folders"] if wc_options else "" if wc_wildcards_folders == "": try: diff --git a/ppp_config.yaml.defaults b/ppp_config.yaml.defaults index 7de7080..b62c606 100644 --- a/ppp_config.yaml.defaults +++ b/ppp_config.yaml.defaults @@ -1,9 +1,12 @@ +# Prompt Post-Processor configuration file. + # If you want to change any of these options make first a copy of this file as "ppp_config.yaml" and edit that file. -# This file will be overwritten when updating the extension, but your custom "ppp_config.yaml" should not be affected. +# In ComfyUI the file should be in the user folder. +# This defaults file will be overwritten when updating the extension, but your custom "ppp_config.yaml" should not be affected. -hosts: # Options for supported host applications - - # For each host define how to handle some operations: +# Options for supported host applications +hosts: + # For each host, define how to handle some operations: # attention: how to handle attention operators # - ok: leave as is # - parentheses: convert to parentheses-based attention @@ -34,7 +37,7 @@ hosts: # Options for supported host applications # - comma: replace with comma # - remove: remove the operator # - error: raise an error - + # Options in the user file override all these options by host (individual options are not inherited and if missing will use defaults in code). a1111: attention: ok scheduling: ok @@ -60,25 +63,198 @@ hosts: # Options for supported host applications and: ok break: ok comfyui: - # comfyui only supports parentheses-based attention + # ComfyUI only supports parentheses-based attention attention: parentheses - # by default comfyui does not support scheduling, alternation, AND or BREAK operators, but you may be able to use them with a custom text encoder node + # by default ComfyUI does not support scheduling, alternation, AND or BREAK operators, but you may be able to use them with a custom text encoder node scheduling: error alternation: error and: comma break: comma -models: # Options for supported base models and variants - sd1: - sd2: - sdxl: +# Supported base models, variants, and options +# Check supported models for each host in: +# A1111: +# Forge: repositories\huggingface_guess\huggingface_guess\model_list.py +# reForge: +# SD.Next: +# ComfyUI: +models: + # We define supported models and how we detect them in each host application (by class or by a known boolean property, or null for not supported). + # We can also define here variants and some options. + # For each model, detect options in the user file replace these ones by host, and variants are fully replaced (so if you want to add to the default ones you need to copy them to your file). + sd1: # Stable Diffusion 1 + detect: + a1111: { property: "is_sd1" } + forge: { property: "is_sd1" } + reforge: { property: "is_sd1" } + sdnext: { class: ["LatentDiffusion", "StableDiffusionPipeline"] } # LatentDiffusion is for the original backend, StableDiffusionPipeline is for the diffusers backend; cannot differentiate SD1 and SD2, we set both to True + comfyui: { class: ["SD15", "SD15_instructpix2pix"] } + sd2: # Stable Diffusion 2 + detect: + a1111: { property: "is_sd2" } + forge: { property: "is_sd2" } + reforge: { property: "is_sd2" } + sdnext: { class: ["LatentDiffusion", "StableDiffusionPipeline"] } # cannot differentiate SD1 and SD2, we set both to True; LatentDiffusion is for the original backend, StableDiffusionPipeline is for the diffusers backend + comfyui: { class: ["SD20", "SD21UnclipL", "SD21UnclipH", "LotusD"] } + ssd: # Segmind Stable Diffusion 1B + detect: + a1111: { property: "is_ssd" } + forge: null + reforge: { property: "is_ssd" } + sdnext: null + comfyui: { class: ["SSD1B"]} + sdxl: # Stable Diffusion XL + detect: + a1111: { property: "is_sdxl" } + forge: { property: "is_sdxl" } + reforge: { property: "is_sdxl" } + sdnext: { class: ["StableDiffusionXLPipeline"] } + comfyui: { class: ["SDXL", "SDXLRefiner", "SDXL_instructpix2pix", "Segmind_Vega", "KOALA_700M", "KOALA_1B"] } variants: # At this level goes the name of the defined variants - pony: - # find_in_filename: regex patterns as a string or list of strings to identify the variant in the model filename (ignores case) - find_in_filename: "(?:pony|pny|pdxl)" - illustrious: - find_in_filename: "(?:illustrious|illust|ilxl)" - sd3: - flux: - auraflow: + # find_in_filename: regex patterns as a str or regex/flags dict or list of them to identify the variant in the model filename. + # If specified as a string, it is considered a regex with the IGNORECASE flag; + # If specified as a dict, it should have "regex" and optional "flags" keys; + # If specified as a list, it should be a list of dicts or strings + pony: { find_in_filename: "(?:pony|pny|pdxl)" } + illustrious: { find_in_filename: { regex: "(?:illustrious|illust|ilxl)", flags: ["IGNORECASE"] } } + sd3: # Stable Diffusion 3 + detect: + a1111: { property: "is_sd3" } + forge: { property: "is_sd3", class: ["SD3"] } + reforge: { property: "is_sd3" } + sdnext: { class: ["StableDiffusion3Pipeline"] } + comfyui: { class: ["SD3"] } + flux: # Flux 1 + detect: + a1111: null + forge: { class: ["Flux", "FluxSchnell"] } + reforge: null + sdnext: { class: ["FluxPipeline"] } + comfyui: { class: ["Flux", "FluxInpaint", "FluxSchnell"] } + auraflow: # AuraFlow + detect: + a1111: null + forge: null + reforge: null + sdnext: { class: ["AuraFlowPipeline"] } + comfyui: { class: ["AuraFlow"] } + pixart: # PixArt + detect: + a1111: null + forge: null + reforge: null + sdnext: { class: ["PixArtAlphaPipeline"] } + comfyui: { class: ["PixArtAlpha", "PixArtSigma"] } + lumina2: # Lumina2 & ZImage + detect: + a1111: null + forge: null + reforge: null + sdnext: null + comfyui: { class: ["Lumina2", "ZImage"] } + ltxv: # LTXV + detect: + a1111: null + forge: null + reforge: null + sdnext: null + comfyui: { class: ["LTXV", "LTXAV"] } + cosmos: # Cosmos + detect: + a1111: null + forge: null + reforge: null + sdnext: null + comfyui: { class: ["CosmosT2V", "CosmosI2V"] } + cosmospredict2: # CosmosPredict2 + detect: + a1111: null + forge: null + reforge: null + sdnext: null + comfyui: { class: ["CosmosT2IPredict2", "CosmosI2VPredict2"] } + genmomochi: # GenmoMochi + detect: + a1111: null + forge: null + reforge: null + sdnext: null + comfyui: { class: ["GenmoMochi"] } + hunyuan: # Hunyuan + detect: + a1111: null + forge: null + reforge: null + sdnext: null + comfyui: { class: ["HunyuanDiT", "HunyuanDiT1"] } + hunyuanvideo: # HunyuanVideo + detect: + a1111: null + forge: null + reforge: null + sdnext: null + comfyui: { class: ["HunyuanVideo", "HunyuanVideoI2V", "HunyuanVideoSkyreelsI2V", "HunyuanImage21", "HunyuanImage21Refiner", "HunyuanVideo15", "HunyuanVideo15_SR_Distilled"] } + hunyuan3d: # Hunyuan3D + detect: + a1111: null + forge: null + reforge: null + sdnext: null + comfyui: { class: ["Hunyuan3Dv2", "Hunyuan3Dv2_1", "Hunyuan3Dv2mini"] } + wan: # Wan21 & Wan22 + detect: + a1111: null + forge: null + reforge: null + sdnext: null + comfyui: { class: ["WAN21_T2V", "WAN21_I2V", "WAN21_FunControl2V", "WAN21_Camera", "WAN22_Camera", "WAN21_Vace", "WAN21_HuMo", "WAN22_S2V", "WAN22_Animate", "WAN22_T2V"] } + hidream: # HiDream + detect: + a1111: null + forge: null + reforge: null + sdnext: null + comfyui: { class: ["HiDream"] } + qwenimage: # QwenImage & QwenImageEdit + detect: + a1111: null + forge: null + reforge: null + sdnext: null + comfyui: { class: ["QwenImage"] } + chroma: # Chroma + detect: + a1111: null + forge: null + reforge: null + sdnext: null + comfyui: { class: ["Chroma", "ChromaRadiance"] } + omnigen2: # Omnigen 2 + detect: + a1111: null + forge: null + reforge: null + sdnext: null + comfyui: { class: ["Omnigen2"] } + flux2: # Flux 2 + detect: + a1111: null + forge: null + reforge: null + sdnext: null + comfyui: { class: ["Flux2"] } + kandinsky5: # Kandinsky 5 + detect: + a1111: null + forge: null + reforge: null + sdnext: null + comfyui: { class: ["Kandinsky5", "Kandinsky5Image"] } + anima: # Anima + detect: + a1111: null + forge: null + reforge: null + sdnext: null + comfyui: { class: ["Anima"] } diff --git a/ppp_enmappings.py b/ppp_enmappings.py index b233b25..3cf3321 100644 --- a/ppp_enmappings.py +++ b/ppp_enmappings.py @@ -66,7 +66,7 @@ class PPPExtraNetworkMappings: """ DEFAULT_ENMAPPINGS_FOLDER = "extranetworkmappings" - LOCALINPUT_FILENAME = "#INPUT" + LOCALINPUT_FILENAME = R"//INPUT\\" def __init__(self, logger): self.__logger: logging.Logger = logger diff --git a/ppp_hosts.py b/ppp_hosts.py index b9c79e9..03ea527 100644 --- a/ppp_hosts.py +++ b/ppp_hosts.py @@ -7,6 +7,7 @@ class SUPPORTED_APPS(Enum): forge = "forge" reforge = "reforge" sdnext = "sdnext" + tests = "tests" # for testing purposes only, not a real app SUPPORTED_APPS_NAMES = { SUPPORTED_APPS.comfyui: "ComfyUI", @@ -14,4 +15,5 @@ SUPPORTED_APPS_NAMES = { SUPPORTED_APPS.forge: "Forge", SUPPORTED_APPS.reforge: "reForge", SUPPORTED_APPS.a1111: "A1111 (or compatible)", + SUPPORTED_APPS.tests: "Tests", } diff --git a/ppp_wildcards.py b/ppp_wildcards.py index 4657dbe..ec0959c 100644 --- a/ppp_wildcards.py +++ b/ppp_wildcards.py @@ -50,7 +50,7 @@ class PPPWildcards: """ DEFAULT_WILDCARDS_FOLDER = "wildcards" - LOCALINPUT_FILENAME = "#INPUT" + LOCALINPUT_FILENAME = R"//INPUT\\" def __init__(self, logger): self.__logger: logging.Logger = logger diff --git a/scripts/ppp_script.py b/scripts/ppp_script.py index b660f3c..5d8f478 100644 --- a/scripts/ppp_script.py +++ b/scripts/ppp_script.py @@ -213,48 +213,11 @@ class PromptPostProcessorA1111Script(scripts.Script): "app": app.value, "models_path": models_path, "model_filename": getattr(p.sd_model.sd_checkpoint_info, "filename", ""), - "model_class": "", + "model_class": p.sd_model.__class__.__name__, + "property_base": p.sd_model, } - env_info.update({"is_" + k: False for k in PromptPostProcessor.KNOWN_MODELS}) - if app == SUPPORTED_APPS.sdnext: - # cannot differentiate SD1 and SD2, we set both to True - # LatentDiffusion is for the original backend, StableDiffusionPipeline is for the diffusers backend - env_info["model_class"] = p.sd_model.__class__.__name__ - env_info["is_sd1"] = p.sd_model.__class__.__name__ in ("LatentDiffusion", "StableDiffusionPipeline") - env_info["is_sd2"] = p.sd_model.__class__.__name__ in ("LatentDiffusion", "StableDiffusionPipeline") - env_info["is_sdxl"] = p.sd_model.__class__.__name__ == "StableDiffusionXLPipeline" - env_info["is_sd3"] = p.sd_model.__class__.__name__ == "StableDiffusion3Pipeline" - env_info["is_flux"] = p.sd_model.__class__.__name__ == "FluxPipeline" - env_info["is_auraflow"] = p.sd_model.__class__.__name__ == "AuraFlowPipeline" - env_info["is_pixart"] = p.sd_model.__class__.__name__ == "PixArtAlphaPipeline" - # also supports 'Latent Consistency Model': LatentConsistencyModelPipeline', 'UniDiffuser': 'UniDiffuserPipeline', 'Wuerstchen': 'WuerstchenCombinedPipeline', 'Kandinsky 2.1': 'KandinskyPipeline', 'Kandinsky 2.2': 'KandinskyV22Pipeline', 'Kandinsky 3': 'Kandinsky3Pipeline', 'DeepFloyd IF': 'IFPipeline', 'Custom Diffusers Pipeline': 'DiffusionPipeline', 'InstaFlow': 'StableDiffusionPipeline', 'SegMoE': 'StableDiffusionPipeline', 'Kolors': 'KolorsPipeline', 'CogView': 'CogView3PlusPipeline' - elif app == SUPPORTED_APPS.forge: - # from repositories\huggingface_guess\huggingface_guess\model_list.py + if app == SUPPORTED_APPS.forge: env_info["model_class"] = p.sd_model.model_config.__class__.__name__ - env_info["is_sd1"] = getattr(p.sd_model, "is_sd1", False) - env_info["is_sd2"] = getattr(p.sd_model, "is_sd2", False) - env_info["is_sdxl"] = getattr(p.sd_model, "is_sdxl", False) - # env_info["is_ssd"] = False # ? - env_info["is_sd3"] = getattr( - p.sd_model, "is_sd3", False - ) # p.sd_model.model_config.__class__.__name__ == "SD3" # not actually supported? - env_info["is_flux"] = p.sd_model.model_config.__class__.__name__ in ("Flux", "FluxSchnell") - # env_info["is_auraflow"] = False # p.sd_model.model_config.__class__.__name__ == "AuraFlow" # not supported - elif app == SUPPORTED_APPS.reforge: - env_info["model_class"] = p.sd_model.__class__.__name__ - env_info["is_sd1"] = getattr(p.sd_model, "is_sd1", False) - env_info["is_sd2"] = getattr(p.sd_model, "is_sd2", False) - env_info["is_sdxl"] = getattr(p.sd_model, "is_sdxl", False) - env_info["is_ssd"] = getattr(p.sd_model, "is_ssd", False) - env_info["is_sd3"] = getattr(p.sd_model, "is_sd3", False) - else: # assume A1111 compatible (p.sd_model.__class__.__name__=="DiffusionEngine") - env_info["model_class"] = p.sd_model.__class__.__name__ - env_info["is_sd1"] = getattr(p.sd_model, "is_sd1", False) - env_info["is_sd2"] = getattr(p.sd_model, "is_sd2", False) - env_info["is_sdxl"] = getattr(p.sd_model, "is_sdxl", False) - env_info["is_ssd"] = getattr(p.sd_model, "is_ssd", False) - env_info["is_sd3"] = getattr(p.sd_model, "is_sd3", False) - hash_envinfo = hash(tuple(sorted(env_info.items()))) wc_wildcards_folders = getattr(opts, "ppp_wil_wildcardsfolders", "") if wc_wildcards_folders == "": wc_wildcards_folders = os.getenv("WILDCARD_DIR", PPPWildcards.DEFAULT_WILDCARDS_FOLDER) @@ -318,7 +281,6 @@ class PromptPostProcessorA1111Script(scripts.Script): opts, "ppp_rem_removeextranetworktags", PromptPostProcessor.DEFAULT_CUP_REMOVE_EXTRANETWORK_TAGS ), } - hash_options = hash(tuple(sorted(options.items()))) self.wildcards_obj.refresh_wildcards( self.ppp_debug_level, wildcards_folders if options["process_wildcards"] else None ) @@ -332,6 +294,8 @@ class PromptPostProcessorA1111Script(scripts.Script): self.wildcards_obj, self.extranetwork_mappings_obj, ) + hash_options = ppp.options_hash() + hash_envinfo = ppp.envinfo_hash() prompts_list = [] if input_force_equal_seeds: diff --git a/tests/tests.py b/tests/tests.py index 576503d..8a5cc34 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -68,8 +68,8 @@ class TestPromptPostProcessorBase(unittest.TestCase): self.def_env_info = { "app": "tests", "ppp_config": None, - "is_sdxl": True, - "model_class": "DiffusionEngine", + "model_class": "SDXL", + "property_base": {"is_sdxl": True}, "models_path": "./webui/models", "model_filename": "./webui/models/Stable-diffusion/testmodel.safetensors", } @@ -196,7 +196,9 @@ class TestPromptPostProcessorBase(unittest.TestCase): self.assertEqual(result_negative_prompt, eo.negative_prompt, "Incorrect negative prompt") if variables is not None: for var_name, var_value in variables.items(): - self.assertIn(var_name, output_variables, f"Variable '{var_name}' not found in output variables") + self.assertIn( + var_name, output_variables, f"Variable '{var_name}' not found in output variables" + ) self.assertEqual( output_variables[var_name], var_value, @@ -1141,7 +1143,10 @@ class TestPromptPostProcessor(TestPromptPostProcessorBase): def test_wc_wildcard_default_filter(self): # wildcard with default filter self.process( - PromptPair("the choice is: __yaml/wildcard2__, __yaml/wildcard2__", ""), + PromptPair( + "the choice is: __yaml/wildcard2__, __yaml/wildcard2__", + "", + ), PromptPair("the choice is: choice3-choice3, choice3-choice1- choice2 ", ""), ppp="nocup", ) @@ -1720,21 +1725,35 @@ class TestPromptPostProcessor(TestPromptPostProcessorBase): "ppp_config": { "models": { "sd1": { + "detect": {"tests": {"class": ["SD15", "SD15_instructpix2pix"]}}, "variants": { "test3": {"find_in_filename": "testmodel"}, "sdxl": {"find_in_filename": "testmodel"}, - } + }, }, "sdxl": { + "detect": { + "tests": { + "class": [ + "SDXL", + "SDXLRefiner", + "SDXL_instructpix2pix", + "Segmind_Vega", + "KOALA_700M", + "KOALA_1B", + ] + } + }, "variants": { "test1": {"find_in_filename": "testmodel"}, "test2": {"find_in_filename": "testmodel"}, - } + }, }, - "invalid": { + "something": { + "detect": {"tests": {"class": ["something"]}}, "variants": { "test4": {"find_in_filename": "testmodel"}, - } + }, }, } }, @@ -1860,7 +1879,7 @@ class TestPromptPostProcessor(TestPromptPostProcessorBase): ), PromptPair("YES", ""), ) - + # NaN/undefined variable integer comparison tests def test_cmd_if_undefined_var_int_compare_warn(self): # undefined var integer compare with on_warning=warn