Files
torzdf fa79d4dcee Initializers to Torch (#1549)
* Update requirements

* Remove docker

* ICNR init to torch

* conv-aware: Output as Tensor rather than Variable

* Bugfix: Omit type object from automodsumm

* bugfix: Tensorboard unit test. Switch Sequential for Functional

* Suppress broken Keras test

* revert failing test
2026-05-29 11:40:03 +01:00

403 lines
16 KiB
Python

#! /usr/env/bin/python3
"""Handles interfacing between Faceswap Configs and ConfigParser .ini files"""
from __future__ import annotations
import logging
import os
import textwrap
import typing as T
from configparser import ConfigParser
from lib.logger import parse_class_init
from lib.utils import get_module_objects, PROJECT_ROOT
if T.TYPE_CHECKING:
from .objects import ConfigSection, ConfigValueType
logger = logging.getLogger(__name__)
class ConfigFile():
"""Handles the interfacing between saved faceswap .ini configs and internal Config objects
Parameters
----------
plugin_group
The plugin group that is requesting a config file
ini_path
Optional path to a .ini config file. ``None`` for default location. Default: ``None``
"""
def __init__(self, plugin_group: str, ini_path: str | None = None) -> None:
parse_class_init(locals())
self._plugin_group = plugin_group
self._file_path = self._get_config_path(ini_path)
self._parser = self._get_new_configparser()
if self._exists: # Load or create new
self.load()
@property
def _exists(self) -> bool:
"""``True`` if the config.ini file exists"""
return os.path.isfile(self._file_path)
def _get_config_path(self, ini_path: str | None) -> str:
"""Return the path to the config file from the calling folder or the provided file
Parameters
----------
ini_path
Path to a config ini file. ``None`` for default location.
Returns
-------
The full path to the configuration file
"""
if ini_path is not None:
if not os.path.isfile(ini_path):
err = f"Config file does not exist at: {ini_path}"
logger.error(err)
raise ValueError(err)
return ini_path
retval = os.path.join(PROJECT_ROOT, "config", f"{self._plugin_group}.ini")
logger.debug("[%s] Config File location: '%s'", os.path.basename(retval), retval)
return retval
def _get_new_configparser(self) -> ConfigParser:
"""Obtain a fresh ConfigParser object and set it to case-sensitive
Returns
-------
A new ConfigParser object set to case-sensitive
"""
retval = ConfigParser(allow_no_value=True)
retval.optionxform = str # type:ignore[assignment,method-assign]
return retval
# I/O
def load(self) -> None:
"""Load values from the saved config ini file into our Config object"""
logger.verbose("[%s] Loading config: '%s'", # type:ignore[attr-defined]
self._plugin_group, self._file_path)
self._parser.read(self._file_path, encoding="utf-8")
def save(self) -> None:
"""Save a config file"""
logger.debug("[%s] %s config: '%s'",
self._plugin_group, "Updating" if self._exists else "Saving", self._file_path)
# TODO in python >= 3.14 this will error when there are delimiters in the comments
with open(self._file_path, "w", encoding="utf-8", errors="replace") as f_cfg_file:
self._parser.write(f_cfg_file)
logger.info("[%s] Saved config: '%s'", self._plugin_group, self._file_path)
# .ini vs Faceswap Config checking
def _sections_synced(self, app_config: dict[str, ConfigSection]) -> bool:
"""Validate that all of the sections within the application config match with all of the
sections in the ini file
Parameters
----------
app_config
The latest configuration settings from the application. Section name is key
Returns
-------
``True`` if application sections and saved ini sections match
"""
given_sections = set(app_config)
loaded_sections = set(self._parser.sections())
retval = given_sections == loaded_sections
if not retval:
logger.debug("[%s] Config sections are not synced: (app: %s, ini: %s)",
self._plugin_group, sorted(given_sections), sorted(loaded_sections))
return retval
def _options_synced(self, app_config: dict[str, ConfigSection]) -> bool:
"""Validate that all of the option names within the application config match with all of
the option names in the ini file
Note
----
As we need to write a new config anyway, we return on the first change found
Parameters
----------
app_config
The latest configuration settings from the application. Section name is key
Returns
-------
``True`` if application option names match with saved ini option names
"""
for name, section in app_config.items():
given_opts = set(opt for opt in section.options)
loaded_opts = set(self._parser[name].keys())
if given_opts != loaded_opts:
logger.debug("[%s:%s] Config options are not synced: (app: %s, ini: %s)",
self._plugin_group, name, sorted(given_opts), sorted(loaded_opts))
return False
return True
def _values_synced(self, app_section: ConfigSection, section: str) -> bool:
"""Validate that all of the option values within the application config match with all of
the option values in the ini file
Parameters
----------
app_section
The latest configuration settings from the application for the given section
section
The section name to check the option values for
Returns
-------
``True`` if application option values match with saved ini option values
"""
# Need to also pull in keys as False is omitted from the set with just values which can
# cause edge-case false negatives
given_vals = set((k, v.ini_value) for k, v in app_section.options.items())
loaded_vals = set((k, v) for k, v in self._parser[section].items())
retval = given_vals == loaded_vals
if not retval:
logger.debug("[%s:%s] Config values are not synced: (app: %s, ini: %s)",
self._plugin_group, section, sorted(given_vals), sorted(loaded_vals))
return retval
def _is_synced_structure(self, app_config: dict[str, ConfigSection]) -> bool:
"""Validate that all the given sections and option names within the application config
match with their corresponding items in the save .ini file
Parameters
----------
app_config
The latest configuration settings from the application. Section name is key
Returns
-------
``True`` if the app config and saved ini config structure match
"""
if not self._sections_synced(app_config):
return False
if not self._options_synced(app_config):
return False
logger.debug("[%s] Configs are synced", self._plugin_group)
return True
# .ini file insertion
def format_help(self, helptext: str, is_section: bool = False) -> str:
"""Format comments for insertion into a config ini file
Parameters
----------
helptext
The help text to be formatted
is_section
``True`` if the help text pertains to a section. ``False`` if it pertains to an option.
Default: ``True``
Returns
-------
The formatted help text
"""
logger.debug("[%s] Formatting help: (helptext: '%s', is_section: '%s')",
self._plugin_group, helptext, is_section)
formatted = ""
for hlp in helptext.split("\n"):
subsequent_indent = "\t\t" if hlp.startswith("\t") else ""
hlp = f"\t- {hlp[1:].strip()}" if hlp.startswith("\t") else hlp
formatted += textwrap.fill(hlp,
100,
tabsize=4,
subsequent_indent=subsequent_indent) + "\n"
helptext = '# {}'.format(formatted[:-1].replace("\n", "\n# ")) # Strip last newline
helptext = helptext.upper() if is_section else f"\n{helptext}"
return helptext
def _insert_section(self, section: str, helptext: str, config: ConfigParser) -> None:
"""Insert a section into the config
Parameters
----------
section
The section title to insert
helptext
The help text for the config section
config
The config parser object to insert the section into.
"""
logger.debug("[%s:%s] Inserting section: (helptext: '%s', config: '%s')",
self._plugin_group, section, helptext, config)
helptext = self.format_help(helptext, is_section=True)
config.add_section(section)
config.set(section, helptext)
def _insert_option(self,
section: str,
name: str,
helptext: str,
value: str,
config: ConfigParser) -> None:
"""Insert an option into a config section
Parameters
----------
section
The section to insert the option into
name
The name of the option to insert
helptext
The help text for the option
value
The value for the option
config
The config parser object to insert the option into
"""
logger.debug(
"[%s:%s] Inserting option: (name: '%s', helptext: %s, value: '%s', config: '%s')",
self._plugin_group, section, name, helptext, value, config)
helptext = self.format_help(helptext, is_section=False)
config.set(section, helptext)
config.set(section, name, value)
def _sync_from_app(self, app_config: dict[str, ConfigSection]) -> None:
"""Update the saved config.ini file from the values stored in the application config
Existing options keep their saved values as per the .ini files. New options are added with
their application defined default value. Options in the .ini file not in application
provided config are removed.
Note
----
A new configuration object is created as comments are stripped from the loaded ini files.
Parameters
----------
app_config
The latest configuration settings from the application. Section name is key
"""
logger.debug("[%s] Syncing from app", self._plugin_group)
parser = self._get_new_configparser() if self._exists else self._parser
for section_name, section in app_config.items():
self._insert_section(section_name, section.helptext, parser)
for name, opt in section.options.items():
value = self._parser.get(section_name, name, fallback=None)
if value is None:
value = opt.ini_value
logger.debug(
"[%s:%s] Setting default value for non-existent config option '%s': '%s'",
self._plugin_group, section_name, name, value)
self._insert_option(section_name, name, opt.helptext, value, parser)
if parser != self._parser:
self._parser = parser
self.save()
# .ini extraction
def _get_converted_value(self, section: str, option: str, datatype: type) -> ConfigValueType:
"""Return a config item from the .ini file in it's correct type.
Parameters
----------
section
The configuration section to obtain the config option for
option
The configuration option to obtain the converted value for
datatype
The type to return the value as
Returns
-------
The selected configuration option in the correct data format
"""
logger.debug("[%s:%s] Getting config item: (option: '%s', datatype: %s)",
self._plugin_group, section, option, datatype)
assert datatype in (bool, int, float, str, list), (
f"Expected (bool, int, float, str, list). Got {datatype}")
retval: ConfigValueType
if datatype == bool:
retval = self._parser.getboolean(section, option)
elif datatype == int:
retval = self._parser.getint(section, option)
elif datatype == float:
retval = self._parser.getfloat(section, option)
else:
retval = self._parser.get(section, option)
logger.debug("[%s:%s] Got config item: (value: %s, type: %s)",
self._plugin_group, section, retval, type(retval))
return retval
def _sync_to_app(self, app_config: dict[str, ConfigSection]) -> None:
"""Update the values in the application config to those loaded from the saved config.ini.
Parameters
----------
app_config
The latest configuration settings from the application. Section name is key
"""
logger.debug("[%s] Syncing to app", self._plugin_group)
for section_name, section in app_config.items():
if self._values_synced(section, section_name):
continue
for opt_name, opt in section.options.items():
if section_name not in self._parser or opt_name not in self._parser[section_name]:
logger.debug("[%s:%s] Skipping new option: '%s'",
self._plugin_group, section_name, opt_name)
continue
ini_opt = self._parser[section_name][opt_name]
if opt.ini_value != ini_opt:
logger.debug("[%s:%s] Updating '%s' from '%s' to '%s'",
self._plugin_group, section_name,
opt_name, ini_opt, opt.ini_value)
opt.set(self._get_converted_value(section_name, opt_name, opt.datatype))
# .ini insertion and extraction
def on_load(self, app_config: dict[str, ConfigSection]) -> None:
"""Check whether there has been any change between the current application config and
the loaded ini config. If so, update the relevant object(s) appropriately. This check will
also create new config.ini files if they do not pre-exist
Parameters
----------
app_config
The latest configuration settings from the application. Section name is key
"""
if not self._exists:
logger.debug("[%s] Creating new ini file", self._plugin_group)
self._sync_from_app(app_config)
if not self._is_synced_structure(app_config):
self._sync_from_app(app_config)
self._sync_to_app(app_config)
def update_from_app(self, app_config: dict[str, ConfigSection]) -> None:
"""Update the config.ini file to those values that are currently in Faceswap's app
config
Parameters
----------
app_config
The latest configuration settings from the application. Section name is key
"""
logger.debug("[%s] Updating saved config", self._plugin_group)
parser = self._get_new_configparser() if self._exists else self._parser
for section_name, section in app_config.items():
self._insert_section(section_name, section.helptext, parser)
for name, opt in section.options.items():
self._insert_option(section_name, name, opt.helptext, opt.ini_value, parser)
if parser != self._parser:
self._parser = parser
self.save()
__all__ = get_module_objects(__name__)