From e211b7bf3942a834e07215b6ed0adbe419a68e5a Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:04:41 -0800 Subject: [PATCH 01/14] Fix Extension type inference --- modules/extensions.py | 3 ++- modules/ui_extensions.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/extensions.py b/modules/extensions.py index 9a3e1f7f6..1063e5eb7 100644 --- a/modules/extensions.py +++ b/modules/extensions.py @@ -1,3 +1,4 @@ +from __future__ import annotations import os from datetime import datetime import git @@ -5,7 +6,7 @@ from modules import shared, errors from modules.paths import extensions_dir, extensions_builtin_dir -extensions = [] +extensions: list[Extension] = [] if not os.path.exists(extensions_dir): os.makedirs(extensions_dir) diff --git a/modules/ui_extensions.py b/modules/ui_extensions.py index 836fe5db6..a44211d7b 100644 --- a/modules/ui_extensions.py +++ b/modules/ui_extensions.py @@ -29,8 +29,8 @@ sort_ordering = { } -def get_installed(ext) -> extensions.Extension: - installed: extensions.Extension = [e for e in extensions.extensions if (e.remote or '').startswith(ext['url'].replace('.git', ''))] +def get_installed(ext): + installed = [e for e in extensions.extensions if (e.remote or '').startswith(ext['url'].replace('.git', ''))] return installed[0] if len(installed) > 0 else None From 2cd6600a386a19c5df46e03cb03b8316452aef01 Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Thu, 18 Dec 2025 17:04:59 -0800 Subject: [PATCH 02/14] Add type annotations to `options_section` --- modules/options.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/options.py b/modules/options.py index 49dc1f9ad..59e62bb06 100644 --- a/modules/options.py +++ b/modules/options.py @@ -1,8 +1,9 @@ +from __future__ import annotations from dataclasses import dataclass from installer import log -def options_section(section_identifier, options_dict): +def options_section(section_identifier: tuple[str, str], options_dict: dict[str, OptionInfo]): for v in options_dict.values(): v.section = section_identifier return options_dict From 05f767cf5f72c00b60cff4f64ec8d5702fc44881 Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Thu, 18 Dec 2025 18:08:54 -0800 Subject: [PATCH 03/14] Keep option type definitions together... and update type annotations. - Since there's no literal for sets, and because functions usually shouldn't be used as parameter defaults, use None as default and then use `set()` as default during init. --- modules/options.py | 7 ++++++- modules/options_handler.py | 9 ++++----- modules/shared.py | 4 ++-- modules/shared_legacy.py | 7 +------ 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/modules/options.py b/modules/options.py index 59e62bb06..d62e79043 100644 --- a/modules/options.py +++ b/modules/options.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from installer import log -def options_section(section_identifier: tuple[str, str], options_dict: dict[str, OptionInfo]): +def options_section(section_identifier: tuple[str, str], options_dict: dict[str, OptionInfo | LegacyOption]): for v in options_dict.values(): v.section = section_identifier return options_dict @@ -109,6 +109,11 @@ class OptionInfo: return f'OptionInfo: label="{self.label}" section="{self.section}" component="{self.component}" default="{self.default}" refresh="{self.refresh is not None}" change="{self.onchange is not None}" args={args} choices={choices}' +class LegacyOption(OptionInfo): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @dataclass class OptionsCategory: id: str diff --git a/modules/options_handler.py b/modules/options_handler.py index a8538a383..3661e930b 100644 --- a/modules/options_handler.py +++ b/modules/options_handler.py @@ -1,13 +1,10 @@ import os import json import threading -from typing import TYPE_CHECKING from modules import cmd_args, errors from modules.json_helpers import readfile, writefile -from modules.shared_legacy import LegacyOption +from modules.options import OptionInfo, LegacyOption from installer import log -if TYPE_CHECKING: - from modules.options import OptionInfo cmd_opts = cmd_args.parse_args() @@ -21,7 +18,9 @@ class Options(): typemap = {int: float} debug = os.environ.get('SD_CONFIG_DEBUG', None) is not None - def __init__(self, options_templates:dict={}, restricted_opts:dict={}): + def __init__(self, options_templates: dict[str, OptionInfo | LegacyOption] = {}, restricted_opts: set[str] | None = None): + if restricted_opts is None: + restricted_opts = set() self.data_labels = options_templates self.restricted_opts = restricted_opts self.data = {k: v.default for k, v in self.data_labels.items()} diff --git a/modules/shared.py b/modules/shared.py index 1ac08ab9a..ae2cd3380 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -18,7 +18,7 @@ from modules.memstats import memory_stats, ram_stats # pylint: disable=unused-im from modules.interrogate.openclip import caption_models, caption_types, get_clip_models, refresh_clip_models from modules.interrogate.vqa import vlm_models, vlm_prompts, vlm_system, vlm_default from modules.ui_components import DropdownEditable -from modules.options import OptionInfo, options_section +from modules.options import OptionInfo, LegacyOption, options_section import modules.memmon import modules.styles import modules.paths as paths @@ -51,7 +51,7 @@ face_restorers = [] yolo = None tab_names = [] extra_networks: list[ExtraNetworksPage] = [] -options_templates = {} +options_templates: dict[str, OptionInfo | LegacyOption] = {} hypernetworks = {} settings_components = {} restricted_opts = { diff --git a/modules/shared_legacy.py b/modules/shared_legacy.py index f96b29fa1..d8f7ec698 100644 --- a/modules/shared_legacy.py +++ b/modules/shared_legacy.py @@ -1,12 +1,7 @@ import os import gradio as gr from modules import paths -from modules.options import OptionInfo, options_section - - -class LegacyOption(OptionInfo): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) +from modules.options import LegacyOption, options_section legacy_options = options_section(('legacy_options', "Legacy options"), { From eeca45c7b2fa82383cdc1cc89fc83050dbf162fc Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Thu, 18 Dec 2025 18:09:32 -0800 Subject: [PATCH 04/14] Update type annotations --- modules/options_handler.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/options_handler.py b/modules/options_handler.py index 3661e930b..9d76b9e04 100644 --- a/modules/options_handler.py +++ b/modules/options_handler.py @@ -1,6 +1,7 @@ import os import json import threading +from typing import Callable from modules import cmd_args, errors from modules.json_helpers import readfile, writefile from modules.options import OptionInfo, LegacyOption @@ -167,7 +168,7 @@ class Options(): self.data['quicksettings_list'] = [i.strip() for i in self.data.get('quicksettings').split(',')] unknown_settings = [] for k, v in self.data.items(): - info: OptionInfo = self.data_labels.get(k, None) + info: OptionInfo | None = self.data_labels.get(k, None) if info is not None: if not info.validate(k, v): self.data[k] = info.default @@ -179,7 +180,7 @@ class Options(): if len(unknown_settings) > 0: log.warning(f"Setting validation: unknown={unknown_settings}") - def onchange(self, key, func, call=True): + def onchange(self, key, func: Callable, call=True): item = self.data_labels.get(key) item.onchange = func if call: From f20591ce5e726df7283ef394a7df279f50ab44a0 Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Thu, 18 Dec 2025 23:27:53 -0800 Subject: [PATCH 05/14] Use updated Callable import Importing from `typing` is considered deprecated since it's literally just internally calling from `collections.abc` --- modules/options_handler.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/options_handler.py b/modules/options_handler.py index 9d76b9e04..03c3fac42 100644 --- a/modules/options_handler.py +++ b/modules/options_handler.py @@ -1,13 +1,17 @@ +from __future__ import annotations import os import json import threading -from typing import Callable +from typing import TYPE_CHECKING from modules import cmd_args, errors from modules.json_helpers import readfile, writefile from modules.options import OptionInfo, LegacyOption from installer import log +if TYPE_CHECKING: + from collections.abc import Callable + cmd_opts = cmd_args.parse_args() compatibility_opts = ['clip_skip', 'uni_pc_lower_order_final', 'uni_pc_order'] From 878f2f5aa65fc34e62ee9e98199b4fcd9a255240 Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Fri, 19 Dec 2025 00:22:57 -0800 Subject: [PATCH 06/14] Use True/False instead of True/None --- modules/options.py | 2 +- modules/ui_settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/options.py b/modules/options.py index d62e79043..f73bff8a7 100644 --- a/modules/options.py +++ b/modules/options.py @@ -19,7 +19,7 @@ class OptionInfo: onchange=None, section=None, refresh=None, - folder=None, + folder=False, submit=None, comment_before='', comment_after='', diff --git a/modules/ui_settings.py b/modules/ui_settings.py index 2f4bcaa2d..fc6bfa1e2 100644 --- a/modules/ui_settings.py +++ b/modules/ui_settings.py @@ -84,7 +84,7 @@ def create_setting_component(key, is_quicksettings=False): with gr.Row(): res = comp(label=info.label, value=fun(), elem_id=elem_id, **args) ui_common.create_refresh_button(res, info.refresh, info.component_args, f"settings_{key}_refresh") - elif info.folder is not None: + elif info.folder: with gr.Row(): res = comp(label=info.label, value=fun(), elem_id=elem_id, elem_classes="folder-selector", **args) else: From f8b812b68efb33da0d759224933c0ed8b1156ec4 Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Fri, 19 Dec 2025 00:30:11 -0800 Subject: [PATCH 07/14] Add Callable type annotation to `onchange` --- modules/options.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/options.py b/modules/options.py index f73bff8a7..09328a428 100644 --- a/modules/options.py +++ b/modules/options.py @@ -1,8 +1,11 @@ from __future__ import annotations from dataclasses import dataclass +from typing import TYPE_CHECKING from installer import log +if TYPE_CHECKING: + from collections.abc import Callable def options_section(section_identifier: tuple[str, str], options_dict: dict[str, OptionInfo | LegacyOption]): for v in options_dict.values(): v.section = section_identifier @@ -16,9 +19,9 @@ class OptionInfo: label="", component=None, component_args=None, - onchange=None, section=None, refresh=None, + onchange: Callable | None = None, folder=False, submit=None, comment_before='', From e2c5fc0a219d21df38947da57fdc42a3c993df49 Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Fri, 19 Dec 2025 15:46:04 -0800 Subject: [PATCH 08/14] Add simple docstring --- modules/options.py | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/options.py b/modules/options.py index 09328a428..2d0c10490 100644 --- a/modules/options.py +++ b/modules/options.py @@ -7,6 +7,7 @@ from installer import log if TYPE_CHECKING: from collections.abc import Callable def options_section(section_identifier: tuple[str, str], options_dict: dict[str, OptionInfo | LegacyOption]): + """Set the `section` value for all OptionInfo/LegacyOption items""" for v in options_dict.values(): v.section = section_identifier return options_dict From 072cab438d523bef0401ef3a05c75747de5f2dc7 Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Fri, 19 Dec 2025 19:17:52 -0800 Subject: [PATCH 09/14] Use Any type for `default` for now --- modules/options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/options.py b/modules/options.py index 2d0c10490..c44a56dd0 100644 --- a/modules/options.py +++ b/modules/options.py @@ -1,6 +1,6 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from installer import log @@ -16,7 +16,7 @@ def options_section(section_identifier: tuple[str, str], options_dict: dict[str, class OptionInfo: def __init__( self, - default=None, + default: Any | None = None, label="", component=None, component_args=None, From 4d4cf0cef168052b62eada22c91df5b1d91feede Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Fri, 19 Dec 2025 21:22:50 -0800 Subject: [PATCH 10/14] Update options typing --- modules/options.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/modules/options.py b/modules/options.py index c44a56dd0..cd48d0487 100644 --- a/modules/options.py +++ b/modules/options.py @@ -5,7 +5,11 @@ from installer import log if TYPE_CHECKING: - from collections.abc import Callable + from collections.abc import Callable, Mapping + from gradio.components import Component + from modules.ui_components import DropdownEditable + + def options_section(section_identifier: tuple[str, str], options_dict: dict[str, OptionInfo | LegacyOption]): """Set the `section` value for all OptionInfo/LegacyOption items""" for v in options_dict.values(): @@ -18,11 +22,11 @@ class OptionInfo: self, default: Any | None = None, label="", - component=None, - component_args=None, - section=None, - refresh=None, + component: type[Component] | type[DropdownEditable] | None = None, + component_args: Mapping | Callable[..., Mapping] | None = None, onchange: Callable | None = None, + section: tuple[str, ...] | None = None, + refresh: Callable | None = None, folder=False, submit=None, comment_before='', @@ -45,7 +49,7 @@ class OptionInfo: self.exclude = ['sd_model_checkpoint', 'sd_model_refiner', 'sd_vae', 'sd_unet', 'sd_text_encoder'] self.dynamic = callable(component_args) args = {} if self.dynamic else (component_args or {}) # executing callable here is too expensive - self.visible = args.get('visible', True) and len(self.label) > 2 + self.visible = args.get('visible', True) and len(self.label) > 2 # type: ignore - Type checking only sees the value of self.dynamic, not the `callable` check def needs_reload_ui(self): return self From b0dc7580998deba583fb7da38d583838c2993736 Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Fri, 19 Dec 2025 23:52:47 -0800 Subject: [PATCH 11/14] Change Mapping type to dict --- modules/options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/options.py b/modules/options.py index cd48d0487..cd3ca0f23 100644 --- a/modules/options.py +++ b/modules/options.py @@ -5,7 +5,7 @@ from installer import log if TYPE_CHECKING: - from collections.abc import Callable, Mapping + from collections.abc import Callable from gradio.components import Component from modules.ui_components import DropdownEditable @@ -23,7 +23,7 @@ class OptionInfo: default: Any | None = None, label="", component: type[Component] | type[DropdownEditable] | None = None, - component_args: Mapping | Callable[..., Mapping] | None = None, + component_args: dict | Callable[..., dict] | None = None, onchange: Callable | None = None, section: tuple[str, ...] | None = None, refresh: Callable | None = None, From 148f16daedb2a770fb91d640e214650504448540 Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Sat, 20 Dec 2025 00:31:13 -0800 Subject: [PATCH 12/14] Move LegacyOption back to shared_legacy.py Seems like Python doesn't actually consider it a circular import in this instance. --- modules/options.py | 6 +----- modules/shared_legacy.py | 7 ++++++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/modules/options.py b/modules/options.py index cd3ca0f23..6e31c8660 100644 --- a/modules/options.py +++ b/modules/options.py @@ -7,6 +7,7 @@ from installer import log if TYPE_CHECKING: from collections.abc import Callable from gradio.components import Component + from modules.shared_legacy import LegacyOption from modules.ui_components import DropdownEditable @@ -117,11 +118,6 @@ class OptionInfo: return f'OptionInfo: label="{self.label}" section="{self.section}" component="{self.component}" default="{self.default}" refresh="{self.refresh is not None}" change="{self.onchange is not None}" args={args} choices={choices}' -class LegacyOption(OptionInfo): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - @dataclass class OptionsCategory: id: str diff --git a/modules/shared_legacy.py b/modules/shared_legacy.py index d8f7ec698..f96b29fa1 100644 --- a/modules/shared_legacy.py +++ b/modules/shared_legacy.py @@ -1,7 +1,12 @@ import os import gradio as gr from modules import paths -from modules.options import LegacyOption, options_section +from modules.options import OptionInfo, options_section + + +class LegacyOption(OptionInfo): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) legacy_options = options_section(('legacy_options', "Legacy options"), { From 1a5f7ef642a1236e926c5336753a5c9742d7d95a Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Sat, 20 Dec 2025 00:34:09 -0800 Subject: [PATCH 13/14] Update OptionInfo import --- modules/options_handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/options_handler.py b/modules/options_handler.py index 03c3fac42..6d7ae67c6 100644 --- a/modules/options_handler.py +++ b/modules/options_handler.py @@ -5,12 +5,13 @@ import threading from typing import TYPE_CHECKING from modules import cmd_args, errors from modules.json_helpers import readfile, writefile -from modules.options import OptionInfo, LegacyOption +from modules.shared_legacy import LegacyOption from installer import log if TYPE_CHECKING: from collections.abc import Callable + from modules.options import OptionInfo cmd_opts = cmd_args.parse_args() compatibility_opts = ['clip_skip', 'uni_pc_lower_order_final', 'uni_pc_order'] From dfe6e889d8d7f498c0533577c2fc99dbe67f0cda Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Sat, 20 Dec 2025 01:42:03 -0800 Subject: [PATCH 14/14] Fix imports --- modules/shared.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/shared.py b/modules/shared.py index ae2cd3380..16a4a0797 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -18,7 +18,7 @@ from modules.memstats import memory_stats, ram_stats # pylint: disable=unused-im from modules.interrogate.openclip import caption_models, caption_types, get_clip_models, refresh_clip_models from modules.interrogate.vqa import vlm_models, vlm_prompts, vlm_system, vlm_default from modules.ui_components import DropdownEditable -from modules.options import OptionInfo, LegacyOption, options_section +from modules.options import OptionInfo, options_section import modules.memmon import modules.styles import modules.paths as paths @@ -27,7 +27,8 @@ from installer import log, print_dict, console, get_version # pylint: disable=un if TYPE_CHECKING: # Behavior modified by __future__.annotations from diffusers import DiffusionPipeline - from ui_extra_networks import ExtraNetworksPage + from modules.shared_legacy import LegacyOption + from modules.ui_extra_networks import ExtraNetworksPage class Backend(Enum):