feat: Remember last used folder for file dialogs (#3290)

* feat: Remember last used folder for file dialogs

This commit introduces a feature to remember the last used folder for various file and folder dialogs within the GUI.

Key changes:

- Modified `KohyaSSGUIConfig` (`kohya_gui/class_gui_config.py`) to store and retrieve a `last_used_folder` value in the `config.toml` file.
- Updated file/folder dialog utility functions in `kohya_gui/common_gui.py` (e.g., `get_folder_path`, `get_file_path`, `get_saveasfilename_path`) to:
    - Accept the `KohyaSSGUIConfig` instance.
    - Use the stored `last_used_folder` as the initial directory for dialogs.
    - Update `last_used_folder` after a successful selection.
- Updated various GUI modules (`class_folders.py`, `wd14_caption_gui.py`, and other captioning utilities) to pass the `KohyaSSGUIConfig` instance to these dialog functions.

This enhancement improves your experience by defaulting file dialogs to the most recently accessed relevant directory, streamlining the workflow for you when you frequently work with specific folders.

* Fix typo

* Fix typos

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
pull/3293/head
bmaltais 2025-06-16 07:39:29 -04:00 committed by GitHub
parent 060c58a319
commit 51d87f5309
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 709 additions and 81 deletions

View File

@ -8,6 +8,7 @@ from .common_gui import (
list_dirs,
setup_environment,
)
from .class_gui_config import KohyaSSGUIConfig # Added import
import os
import sys
@ -121,7 +122,7 @@ def caption_images(
# Gradio UI
def gradio_basic_caption_gui_tab(headless=False, default_images_dir=None):
def gradio_basic_caption_gui_tab(headless=False, default_images_dir=None, config: KohyaSSGUIConfig = {}): # Added config
"""
Creates a Gradio tab for basic image captioning.
@ -191,7 +192,7 @@ def gradio_basic_caption_gui_tab(headless=False, default_images_dir=None):
)
# Event handler for button click
folder_button.click(
get_folder_path,
lambda: get_folder_path(config=config), # Added config
outputs=images_dir,
show_progress=False,
)

View File

@ -6,6 +6,7 @@ import os
from .common_gui import get_folder_path, scriptdir, list_dirs
from .custom_logging import setup_logging
from .class_gui_config import KohyaSSGUIConfig # Added import
# Set up logging
log = setup_logging()
@ -208,7 +209,7 @@ def caption_images_nucleus(
)
def gradio_blip2_caption_gui_tab(headless=False, directory_path=None):
def gradio_blip2_caption_gui_tab(headless=False, directory_path=None, config: KohyaSSGUIConfig = {}): # Added config
from .common_gui import create_refresh_button
directory_path = (
@ -249,7 +250,7 @@ def gradio_blip2_caption_gui_tab(headless=False, directory_path=None):
visible=(not headless),
)
button_directory_path_dir_input.click(
get_folder_path,
lambda: get_folder_path(config=config), # Added config
outputs=directory_path_dir,
show_progress=False,
)

View File

@ -4,6 +4,7 @@ import os
import sys
from .common_gui import get_folder_path, add_pre_postfix, scriptdir, list_dirs, setup_environment
from .custom_logging import setup_logging
from .class_gui_config import KohyaSSGUIConfig # Added import
# Set up logging
log = setup_logging()
@ -112,7 +113,7 @@ def caption_images(
###
def gradio_blip_caption_gui_tab(headless=False, default_train_dir=None):
def gradio_blip_caption_gui_tab(headless=False, default_train_dir=None, config: KohyaSSGUIConfig = {}): # Added config
from .common_gui import create_refresh_button
default_train_dir = (
@ -152,7 +153,7 @@ def gradio_blip_caption_gui_tab(headless=False, default_train_dir=None):
visible=(not headless),
)
button_train_data_dir_input.click(
get_folder_path,
lambda: get_folder_path(config=config), # Added config
outputs=train_data_dir,
show_progress=False,
)

View File

@ -126,7 +126,7 @@ class Folders:
)
# Output directory button click event
self.output_dir_folder.click(
get_folder_path,
lambda: get_folder_path(config=self.config),
outputs=self.output_dir,
show_progress=False,
)
@ -161,7 +161,7 @@ class Folders:
)
# Regularisation directory button click event
self.reg_data_dir_folder.click(
get_folder_path,
lambda: get_folder_path(config=self.config),
outputs=self.reg_data_dir,
show_progress=False,
)
@ -192,7 +192,7 @@ class Folders:
)
# Logging directory button click event
self.logging_dir_folder.click(
get_folder_path,
lambda: get_folder_path(config=self.config),
outputs=self.logging_dir,
show_progress=False,
)

View File

@ -16,6 +16,8 @@ class KohyaSSGUIConfig:
Initialize the KohyaSSGUIConfig class.
"""
self.config = self.load_config(config_file_path=config_file_path)
# Initialize last_used_folder during class initialization
self.get_last_used_folder()
def load_config(self, config_file_path: str = "./config.toml") -> dict:
"""
@ -35,6 +37,11 @@ class KohyaSSGUIConfig:
f"No configuration file found at {config_file_path}. Initializing empty configuration."
)
# Ensure last_used_folder is present, defaulting to scriptdir
if "last_used_folder" not in config:
config["last_used_folder"] = scriptdir
log.debug(f"Initialized 'last_used_folder' to '{scriptdir}'")
return config
def save_config(self, config: dict, config_file_path: str = "./config.toml"):
@ -91,3 +98,37 @@ class KohyaSSGUIConfig:
is_loaded = self.config != {}
log.debug(f"Configuration was loaded from file: {is_loaded}")
return is_loaded
def get_last_used_folder(self) -> str:
"""
Retrieves the last used folder from the configuration.
Returns:
str: The last used folder path.
"""
folder = self.config.get("last_used_folder", scriptdir)
# Ensure that the returned path is a string, even if it's empty or None from config
if not isinstance(folder, str):
folder = str(folder) if folder is not None else scriptdir
# Update the config if the type was wrong.
self.config["last_used_folder"] = folder
log.debug(f"Retrieved last_used_folder: {folder}")
return folder
def set_last_used_folder(self, folder_path: str):
"""
Sets the last used folder in the configuration.
Parameters:
- folder_path (str): The path to the folder to be set.
"""
if not isinstance(folder_path, str):
log.error(f"Attempted to set last_used_folder with non-string value: {folder_path}")
# Optionally, raise an error or convert, for now, let's default to scriptdir
# or handle as an error appropriately depending on desired behavior.
# For robustness, let's ensure it's always a string or default if invalid.
folder_path = scriptdir
self.config["last_used_folder"] = folder_path
log.debug(f"Set last_used_folder to: {folder_path}")
# Note: Saving the config should be handled explicitly by the caller if needed immediately.
# For example, by calling gui_config.save_config(gui_config.config)

View File

@ -3,10 +3,13 @@ try:
except ImportError:
pass
from easygui import msgbox, ynbox
from typing import Optional
from typing import Optional, TYPE_CHECKING
from .custom_logging import setup_logging
from .sd_modeltype import SDModelType
if TYPE_CHECKING:
from .class_gui_config import KohyaSSGUIConfig
import os
import re
import gradio as gr
@ -457,11 +460,12 @@ def get_dir_and_file(file_path):
def get_file_path(
file_path="", default_extension=".json", extension_name="Config files"
file_path="", default_extension=".json", extension_name="Config files", config: "KohyaSSGUIConfig" = None
):
"""
Opens a file dialog to select a file, allowing the user to navigate and choose a file with a specific extension.
If no file is selected, returns the initially provided file path or an empty string if not provided.
Uses and updates last_used_folder from the config if provided.
This function is conditioned to skip the file dialog on macOS or if specific environment variables are present,
indicating a possible automated environment where a dialog cannot be displayed.
@ -492,11 +496,16 @@ def get_file_path(
if not any(var in os.environ for var in ENV_EXCLUSION) and sys.platform != "darwin":
current_file_path = file_path # Backup in case no file is selected
if not os.path.dirname(file_path):
initial_dir = scriptdir
else:
initial_dir = os.path.dirname(file_path)
initial_file = os.path.basename(file_path)
initial_dir_to_use, initial_file = get_dir_and_file(file_path)
if config:
last_used = config.get_last_used_folder()
if last_used and os.path.isdir(last_used):
initial_dir_to_use = last_used
if not initial_dir_to_use or not os.path.isdir(initial_dir_to_use): # Check if valid dir
initial_dir_to_use = scriptdir
# Initialize a hidden Tkinter window for the file dialog
root = Tk()
@ -504,27 +513,38 @@ def get_file_path(
root.withdraw() # Hide the root window to show only the dialog
# Open the file dialog and capture the selected file path
file_path = filedialog.askopenfilename(
returned_path = filedialog.askopenfilename(
filetypes=((extension_name, f"*{default_extension}"), ("All files", "*.*")),
defaultextension=default_extension,
initialfile=initial_file,
initialdir=initial_dir,
initialdir=initial_dir_to_use,
)
root.destroy() # Cleanup by destroying the Tkinter root window
# Fallback to the initial path if no selection is made
if not file_path:
if returned_path: # User selected a file
file_path = returned_path
if config:
config.set_last_used_folder(os.path.dirname(file_path))
else: # User cancelled dialog
file_path = current_file_path
# Do not update last_used_folder if dialog is cancelled
else:
# For non-dialog environments (headless/macOS)
# If a file_path is provided, is a file, and config is available,
# update last_used_folder based on its directory.
if file_path and os.path.isfile(file_path) and config:
config.set_last_used_folder(os.path.dirname(file_path))
# Return the selected or fallback file path
return file_path
def get_any_file_path(file_path: str = "") -> str:
def get_any_file_path(file_path: str = "", config: "KohyaSSGUIConfig" = None) -> str:
"""
Opens a file dialog to select any file, allowing the user to navigate and choose a file.
If no file is selected, returns the initially provided file path or an empty string if not provided.
Uses and updates last_used_folder from the config if provided.
This function is conditioned to skip the file dialog on macOS or if specific environment variables are present,
indicating a possible automated environment where a dialog cannot be displayed.
@ -555,7 +575,15 @@ def get_any_file_path(file_path: str = "") -> str:
):
current_file_path: str = file_path
initial_dir, initial_file = get_dir_and_file(file_path)
initial_dir_to_use, initial_file = get_dir_and_file(file_path)
if config:
last_used = config.get_last_used_folder()
if last_used and os.path.isdir(last_used):
initial_dir_to_use = last_used
if not initial_dir_to_use or not os.path.isdir(initial_dir_to_use): # Check if valid dir
initial_dir_to_use = scriptdir
# Initialize a hidden Tkinter window for the file dialog
root = Tk()
@ -564,8 +592,8 @@ def get_any_file_path(file_path: str = "") -> str:
try:
# Open the file dialog and capture the selected file path
file_path = filedialog.askopenfilename(
initialdir=initial_dir,
returned_path = filedialog.askopenfilename(
initialdir=initial_dir_to_use, # Use determined initial_dir
initialfile=initial_file,
)
except Exception as e:
@ -573,9 +601,17 @@ def get_any_file_path(file_path: str = "") -> str:
finally:
root.destroy()
# Fallback to the initial path if no selection is made
if not file_path:
if returned_path: # User selected a file
file_path = returned_path
if config:
config.set_last_used_folder(os.path.dirname(file_path))
else: # User cancelled dialog
file_path = current_file_path
# Do not update last_used_folder if dialog is cancelled
else:
# For non-dialog environments
if file_path and os.path.isfile(file_path) and config:
config.set_last_used_folder(os.path.dirname(file_path))
except KeyError as e:
raise EnvironmentError(f"Failed to access environment variables: {e}")
@ -583,10 +619,11 @@ def get_any_file_path(file_path: str = "") -> str:
return file_path
def get_folder_path(folder_path: str = "") -> str:
def get_folder_path(folder_path: str = "", config: "KohyaSSGUIConfig" = None) -> str:
"""
Opens a folder dialog to select a folder, allowing the user to navigate and choose a folder.
If no folder is selected, returns the initially provided folder path or an empty string if not provided.
Uses and updates last_used_folder from the config if provided.
This function is conditioned to skip the folder dialog on macOS or if specific environment variables are present,
indicating a possible automated environment where a dialog cannot be displayed.
@ -609,70 +646,109 @@ def get_folder_path(folder_path: str = "") -> str:
if not isinstance(folder_path, str):
raise TypeError("folder_path must be a string")
initial_dir_to_use = scriptdir # Default initial directory
if config:
last_used = config.get_last_used_folder()
if last_used and os.path.isdir(last_used):
initial_dir_to_use = last_used
elif folder_path and os.path.isdir(folder_path): # Fallback to folder_path if last_used is invalid
initial_dir_to_use = folder_path
# If both last_used and folder_path are invalid or not provided, scriptdir is used as already set
elif folder_path and os.path.isdir(folder_path): # No config, but folder_path is valid
initial_dir_to_use = folder_path
try:
# Check for environment variable conditions
# Check for environment variable conditions to skip dialog
if any(var in os.environ for var in ENV_EXCLUSION) or sys.platform == "darwin":
# Even if dialog is skipped, if a valid folder_path is given and config is present,
# consider it as the "selected" path for updating last_used_folder.
if folder_path and os.path.isdir(folder_path) and config:
config.set_last_used_folder(folder_path)
return folder_path or ""
root = Tk()
root.withdraw()
root.wm_attributes("-topmost", 1)
selected_folder = filedialog.askdirectory(initialdir=folder_path or ".")
# Use initial_dir_to_use, which has been determined based on config or folder_path
selected_folder = filedialog.askdirectory(initialdir=initial_dir_to_use)
root.destroy()
return selected_folder or folder_path
if selected_folder: # User selected a folder
if config:
config.set_last_used_folder(selected_folder)
return selected_folder
else: # User cancelled dialog
# Return the original folder_path or empty string, do not update last_used_folder
return folder_path
except Exception as e:
raise RuntimeError(f"Error initializing folder dialog: {e}") from e
# Log the exception or handle it as per application's error handling policy
log.error(f"Error initializing folder dialog: {e}")
# Fallback to returning the original folder_path in case of an unexpected error
return folder_path
def get_saveasfile_path(
file_path: str = "",
defaultextension: str = ".json",
extension_name: str = "Config files",
config: "KohyaSSGUIConfig" = None,
) -> str:
"""
Opens a file dialog to select a file name for saving, allowing the user to specify a file name and location.
If no file is selected, returns the initially provided file path or an empty string if not provided.
Uses and updates last_used_folder from the config if provided.
Note: This function now uses asksaveasfilename for consistency.
"""
# Check if the current environment is not macOS and if the environment variables do not match the exclusion list
if not any(var in os.environ for var in ENV_EXCLUSION) and sys.platform != "darwin":
# Store the initial file path to use as a fallback in case no file is selected
current_file_path = file_path
# Logging the current file path for debugging purposes; helps in tracking the flow of file selection
# log.info(f'current file path: {current_file_path}')
initial_dir_to_use, initial_file = get_dir_and_file(file_path)
if config:
last_used = config.get_last_used_folder()
if last_used and os.path.isdir(last_used):
initial_dir_to_use = last_used
# Split the file path into directory and file name for setting the file dialog start location and filename
initial_dir, initial_file = get_dir_and_file(file_path)
if not initial_dir_to_use or not os.path.isdir(initial_dir_to_use): # Check if valid dir
initial_dir_to_use = scriptdir
# Initialize a hidden Tkinter window to act as the parent for the file dialog, ensuring it appears on top
root = Tk()
root.wm_attributes("-topmost", 1)
root.withdraw()
save_file_path = filedialog.asksaveasfile(
# Using asksaveasfilename to get the path string directly
returned_path = filedialog.asksaveasfilename(
filetypes=(
(f"{extension_name}", f"{defaultextension}"),
("All files", "*"),
(f"{extension_name}", f"*{defaultextension}"), # Ensure wildcard for extension
("All files", "*.*"),
),
defaultextension=defaultextension,
initialdir=initial_dir,
initialdir=initial_dir_to_use,
initialfile=initial_file,
)
# Close the Tkinter root window to clean up the UI
root.destroy()
# Logging the save file path for auditing purposes; useful in confirming the user's file choice
# log.info(save_file_path)
# Default to the current file path if no file is selected, ensuring there's always a valid file path
if save_file_path == None:
if returned_path: # User selected a path
file_path = returned_path
if config:
config.set_last_used_folder(os.path.dirname(file_path))
else: # User cancelled dialog
file_path = current_file_path
else:
# Log the selected file name for transparency and tracking user actions
# log.info(save_file_path.name)
# Do not update last_used_folder if dialog is cancelled
else:
# For non-dialog environments
# If a file_path is provided (even if it doesn't exist yet, its dir might be valid)
# and config is available, update last_used_folder.
if file_path and config:
dir_name = os.path.dirname(file_path)
if dir_name and os.path.isdir(dir_name): # Check if directory is valid
config.set_last_used_folder(dir_name)
elif not dir_name: # Path is likely just a filename, use scriptdir
config.set_last_used_folder(scriptdir)
# Update the file path with the user-selected file name, facilitating the save operation
file_path = save_file_path.name
# Log the final file path for verification, ensuring the intended file is being used
# log.info(file_path)
# Return the final file path, either the user-selected file or the fallback path
return file_path
@ -680,10 +756,12 @@ def get_saveasfilename_path(
file_path: str = "",
extensions: str = "*",
extension_name: str = "Config files",
config: "KohyaSSGUIConfig" = None,
) -> str:
"""
Opens a file dialog to select a file name for saving, allowing the user to specify a file name and location.
If no file is selected, returns the initially provided file path or an empty string if not provided.
Uses and updates last_used_folder from the config if provided.
This function is conditioned to skip the file dialog on macOS or if specific environment variables are present,
indicating a possible automated environment where a dialog cannot be displayed.
@ -708,36 +786,48 @@ def get_saveasfilename_path(
if not any(var in os.environ for var in ENV_EXCLUSION) and sys.platform != "darwin":
# Store the initial file path to use as a fallback in case no file is selected
current_file_path: str = file_path
# log.info(f'current file path: {current_file_path}')
# Split the file path into directory and file name for setting the file dialog start location and filename
initial_dir, initial_file = get_dir_and_file(file_path)
initial_dir_to_use, initial_file = get_dir_and_file(file_path)
if config:
last_used = config.get_last_used_folder()
if last_used and os.path.isdir(last_used):
initial_dir_to_use = last_used
if not initial_dir_to_use or not os.path.isdir(initial_dir_to_use): # Check if valid dir
initial_dir_to_use = scriptdir
# Initialize a hidden Tkinter window to act as the parent for the file dialog, ensuring it appears on top
root = Tk()
root.wm_attributes("-topmost", 1)
root.withdraw()
# Open the file dialog and capture the selected file path
save_file_path = filedialog.asksaveasfilename(
returned_path = filedialog.asksaveasfilename(
filetypes=(
(f"{extension_name}", f"{extensions}"),
("All files", "*"),
(f"{extension_name}", f"{extensions}"), # Corrected: uses extensions parameter
("All files", "*.*"),
),
defaultextension=extensions,
initialdir=initial_dir,
defaultextension=extensions, # Corrected: uses extensions parameter
initialdir=initial_dir_to_use,
initialfile=initial_file,
)
# Close the Tkinter root window to clean up the UI
root.destroy()
# Default to the current file path if no file is selected, ensuring there's always a valid file path
if save_file_path == "":
if returned_path: # User selected a path
file_path = returned_path
if config:
config.set_last_used_folder(os.path.dirname(file_path))
else: # User cancelled dialog
file_path = current_file_path
else:
# Logging the save file path for auditing purposes; useful in confirming the user's file choice
# log.info(save_file_path)
# Update the file path with the user-selected file name, facilitating the save operation
file_path = save_file_path
# Do not update last_used_folder if dialog is cancelled
else:
# For non-dialog environments
if file_path and config:
dir_name = os.path.dirname(file_path)
if dir_name and os.path.isdir(dir_name):
config.set_last_used_folder(dir_name)
elif not dir_name: # Path is likely just a filename, use scriptdir
config.set_last_used_folder(scriptdir)
# Return the final file path, either the user-selected file or the fallback path
return file_path

View File

@ -3,6 +3,7 @@ import subprocess
import os
import sys
from .common_gui import get_folder_path, add_pre_postfix, scriptdir, list_dirs, setup_environment
from .class_gui_config import KohyaSSGUIConfig # Added import
from .custom_logging import setup_logging
@ -85,7 +86,7 @@ def caption_images(
def gradio_git_caption_gui_tab(
headless=False, default_train_dir=None,
headless=False, default_train_dir=None, config: KohyaSSGUIConfig = {} # Added config
):
from .common_gui import create_refresh_button
@ -126,7 +127,7 @@ def gradio_git_caption_gui_tab(
visible=(not headless),
)
button_train_data_dir_input.click(
get_folder_path,
lambda: get_folder_path(config=config), # Added config
outputs=train_data_dir,
show_progress=False,
)

View File

@ -1,6 +1,7 @@
import gradio as gr
from easygui import msgbox, boolbox
from .common_gui import get_folder_path, scriptdir, list_dirs
from .class_gui_config import KohyaSSGUIConfig # Added import
from math import ceil
import os
import re
@ -250,7 +251,7 @@ def update_images(
# Gradio UI
def gradio_manual_caption_gui_tab(headless=False, default_images_dir=None):
def gradio_manual_caption_gui_tab(headless=False, default_images_dir=None, config: KohyaSSGUIConfig = {}): # Added config
from .common_gui import create_refresh_button
default_images_dir = (
@ -293,7 +294,7 @@ def gradio_manual_caption_gui_tab(headless=False, default_images_dir=None):
visible=(not headless),
)
folder_button.click(
get_folder_path,
lambda: get_folder_path(config=config), # Added config
outputs=images_dir,
show_progress=False,
)

View File

@ -20,12 +20,12 @@ def utilities_tab(
config: KohyaSSGUIConfig = {},
):
with gr.Tab("Captioning"):
gradio_basic_caption_gui_tab(headless=headless)
gradio_blip_caption_gui_tab(headless=headless)
gradio_blip2_caption_gui_tab(headless=headless)
gradio_git_caption_gui_tab(headless=headless)
gradio_basic_caption_gui_tab(headless=headless, config=config)
gradio_blip_caption_gui_tab(headless=headless, config=config)
gradio_blip2_caption_gui_tab(headless=headless, config=config)
gradio_git_caption_gui_tab(headless=headless, config=config)
gradio_wd14_caption_gui_tab(headless=headless, config=config)
gradio_manual_caption_gui_tab(headless=headless)
gradio_manual_caption_gui_tab(headless=headless, config=config)
gradio_convert_model_tab(headless=headless)
gradio_group_images_gui_tab(headless=headless)

View File

@ -190,7 +190,7 @@ def gradio_wd14_caption_gui_tab(
visible=(not headless),
)
button_train_data_dir_input.click(
get_folder_path,
lambda: get_folder_path(config=config),
outputs=train_data_dir,
show_progress=False,
)

61
test_script_1.py Normal file
View File

@ -0,0 +1,61 @@
from kohya_gui.class_gui_config import KohyaSSGUIConfig
from kohya_gui.common_gui import get_folder_path, scriptdir
import os
# Ensure the target directory exists
os.makedirs("/tmp/test_folder1", exist_ok=True)
# 1. Create config (simulating app launch)
config_handler = KohyaSSGUIConfig(config_file_path="/app/config.toml")
print(f"Initial last_used_folder from config: {config_handler.get_last_used_folder()}") # Should be scriptdir
# 2. Simulate get_folder_path
# Normally, tkinter dialog would run. We simulate its effect.
# get_folder_path internally calls config.set_last_used_folder
# Let's assume the dialog was opened with initialdir=scriptdir and user selected /tmp/test_folder1
# For the test, we'll directly set it after a simulated selection to mimic the function's behavior
# In a real scenario, get_folder_path would be called, and it would call set_last_used_folder.
# Here, we simplify by directly manipulating for test verification.
# Simulate call to get_folder_path where user selects /tmp/test_folder1
# This is a simplified mock of what would happen:
# initial_dir = config_handler.get_last_used_folder() # This would be scriptdir
# print(f"Dialog would open with initial_dir: {initial_dir}")
# selected_path_by_user = "/tmp/test_folder1" # User selects this
# if selected_path_by_user:
# config_handler.set_last_used_folder(selected_path_by_user)
# print(f"Set last_used_folder to: {selected_path_by_user}")
# This is what happens inside get_folder_path:
def mock_get_folder_path(current_path_in_field, cfg_obj, simulated_user_selection):
# Logic from common_gui.get_folder_path for initial_dir_to_use
initial_dir_to_use = scriptdir
if cfg_obj:
last_used = cfg_obj.get_last_used_folder()
if last_used and os.path.isdir(last_used):
initial_dir_to_use = last_used
elif current_path_in_field and os.path.isdir(current_path_in_field):
initial_dir_to_use = current_path_in_field
elif current_path_in_field and os.path.isdir(current_path_in_field):
initial_dir_to_use = current_path_in_field
print(f"Dialog would open with initial_dir: {initial_dir_to_use}")
# Simulate user selection
if simulated_user_selection:
if cfg_obj:
cfg_obj.set_last_used_folder(simulated_user_selection)
print(f"Set last_used_folder to: {simulated_user_selection}")
return simulated_user_selection
return current_path_in_field
# Simulate the scenario: field is empty, user selects /tmp/test_folder1
returned_path = mock_get_folder_path("", config_handler, "/tmp/test_folder1")
print(f"Returned path: {returned_path}")
# 3. Save config
config_handler.save_config(config=config_handler.config, config_file_path="/app/config.toml")
print("Config saved.")
# Verify by reloading
config_handler_verify = KohyaSSGUIConfig(config_file_path="/app/config.toml")
print(f"After save, last_used_folder from new config: {config_handler_verify.get_last_used_folder()}")

135
test_script_1_mocked.py Normal file
View File

@ -0,0 +1,135 @@
import os
import sys
import unittest.mock as mock
# Mock easygui and its problematic dependencies before they are imported by kohya_gui
sys.modules['easygui'] = mock.MagicMock()
sys.modules['tkinter'] = mock.MagicMock()
sys.modules['Tkinter'] = mock.MagicMock() # For Python 2 fallback in easygui
# Attempt to preemptively mock parts of easygui that might still load
# This is to handle the "global_state" not found if easygui's __init__ tries to import its own modules
# that might then fail on tkinter not being truly available.
mock_easygui = mock.MagicMock()
mock_easygui.msgbox = mock.MagicMock()
mock_easygui.ynbox = mock.MagicMock()
sys.modules['easygui'] = mock_easygui
# Set an env var to simulate a non-interactive environment (though mocking should handle most UI calls)
os.environ["COLAB_GPU"] = "True"
from kohya_gui.class_gui_config import KohyaSSGUIConfig
from kohya_gui.common_gui import get_folder_path, scriptdir, get_file_path, get_saveasfilename_path # Import other functions as needed
print(f"Scriptdir resolved to: {scriptdir}")
# Ensure scriptdir is an absolute path for consistency, as it's used for defaults
if not os.path.isabs(scriptdir):
scriptdir = os.path.abspath(scriptdir)
print(f"Updated scriptdir to absolute path: {scriptdir}")
# --- Test 1: Initial state and first folder operation ---
print("--- Test 1: Initial Folder Operation ---")
config_handler = KohyaSSGUIConfig(config_file_path="/app/config.toml")
initial_last_folder = config_handler.get_last_used_folder()
print(f"Initial last_used_folder from new config object: {initial_last_folder}")
# Depending on KohyaSSGUIConfig implementation, initial value might be scriptdir or empty string
# before first save. get_last_used_folder itself initializes it to scriptdir if not in config.
assert initial_last_folder == scriptdir, f"Expected {scriptdir}, got {initial_last_folder}"
# Simulate selecting /tmp/test_folder1 for a folder operation
# In non-dialog mode (due to COLAB_GPU=True), get_folder_path should use the provided 'folder_path'
# and update the config.
returned_path = get_folder_path(folder_path="/tmp/test_folder1", config=config_handler)
print(f"Returned path from get_folder_path: {returned_path}")
assert returned_path == "/tmp/test_folder1"
current_last_folder = config_handler.get_last_used_folder()
print(f"Last used folder after get_folder_path: {current_last_folder}")
assert current_last_folder == "/tmp/test_folder1", f"Expected /tmp/test_folder1, got {current_last_folder}"
config_handler.save_config(config=config_handler.config, config_file_path="/app/config.toml")
print("Config saved.")
with open("/app/config.toml", "r") as f:
content = f.read()
print(f"config.toml content:\n{content}")
assert 'last_used_folder = "/tmp/test_folder1"' in content
print("--- Test 1 Passed ---")
# --- Test 2: Relaunch and second folder operation ---
print("\n--- Test 2: Relaunch and Second Folder Operation ---")
config_handler_relaunch = KohyaSSGUIConfig(config_file_path="/app/config.toml")
relaunch_last_folder = config_handler_relaunch.get_last_used_folder()
print(f"On relaunch, last_used_folder is: {relaunch_last_folder}")
assert relaunch_last_folder == "/tmp/test_folder1", f"Expected /tmp/test_folder1, got {relaunch_last_folder}"
# Simulate selecting /tmp/test_folder2
returned_path_2 = get_folder_path(folder_path="/tmp/test_folder2", config=config_handler_relaunch)
print(f"Returned path from second get_folder_path: {returned_path_2}")
assert returned_path_2 == "/tmp/test_folder2"
current_last_folder_2 = config_handler_relaunch.get_last_used_folder()
print(f"Last used folder after second get_folder_path: {current_last_folder_2}")
assert current_last_folder_2 == "/tmp/test_folder2", f"Expected /tmp/test_folder2, got {current_last_folder_2}"
config_handler_relaunch.save_config(config=config_handler_relaunch.config, config_file_path="/app/config.toml")
print("Config saved for second operation.")
with open("/app/config.toml", "r") as f:
content_2 = f.read()
print(f"config.toml content after second operation:\n{content_2}")
assert 'last_used_folder = "/tmp/test_folder2"' in content_2
print("--- Test 2 Passed ---")
print("\nInitial State Check (Folders) successful.")
# --- Test 3: File open operation ---
print("\n--- Test 3: File Open Operation ---")
# Ensure target directory for file exists
os.makedirs("/tmp/test_config_folder", exist_ok=True) # Changed from /projects to /tmp
dummy_file_path = "/tmp/test_config_folder/my_config.json" # Changed from /projects to /tmp
if not os.path.exists(dummy_file_path):
with open(dummy_file_path, "w") as f: f.write("{}")
# Config handler is config_handler_relaunch, which has last_used_folder = /tmp/test_folder2
# Simulate opening a file
# In non-dialog mode, get_file_path will set last_used_folder to dirname of 'file_path'
returned_file_path = get_file_path(file_path=dummy_file_path, config=config_handler_relaunch)
print(f"Returned path from get_file_path: {returned_file_path}")
assert returned_file_path == dummy_file_path
current_last_folder_3 = config_handler_relaunch.get_last_used_folder()
print(f"Last used folder after get_file_path: {current_last_folder_3}")
assert current_last_folder_3 == "/tmp/test_config_folder", f"Expected /tmp/test_config_folder, got {current_last_folder_3}" # Changed from /projects to /tmp
config_handler_relaunch.save_config(config=config_handler_relaunch.config, config_file_path="/app/config.toml")
with open("/app/config.toml", "r") as f:
content_3 = f.read()
print(f"config.toml content after file open:\n{content_3}")
assert 'last_used_folder = "/tmp/test_config_folder"' in content_3 # Changed from /projects to /tmp
print("--- Test 3 Passed ---")
# --- Test 4: File save operation ---
print("\n--- Test 4: File Save Operation ---")
# Ensure target directory for save exists
os.makedirs("/tmp/another_save_folder", exist_ok=True) # Changed from /projects to /tmp
save_file_as_path = "/tmp/another_save_folder/my_new_config.json" # Changed from /projects to /tmp
# Config handler still has last_used_folder = /tmp/test_config_folder
# Simulate saving a file
# In non-dialog mode, get_saveasfilename_path will set last_used_folder to dirname of 'file_path'
returned_save_path = get_saveasfilename_path(file_path=save_file_as_path, config=config_handler_relaunch)
print(f"Returned path from get_saveasfilename_path: {returned_save_path}")
assert returned_save_path == save_file_as_path # In non-dialog mode, it returns the path given
current_last_folder_4 = config_handler_relaunch.get_last_used_folder()
print(f"Last used folder after get_saveasfilename_path: {current_last_folder_4}")
assert current_last_folder_4 == "/tmp/another_save_folder", f"Expected /tmp/another_save_folder, got {current_last_folder_4}" # Changed from /projects to /tmp
config_handler_relaunch.save_config(config=config_handler_relaunch.config, config_file_path="/app/config.toml")
with open("/app/config.toml", "r") as f:
content_4 = f.read()
print(f"config.toml content after file save:\n{content_4}")
assert 'last_used_folder = "/tmp/another_save_folder"' in content_4 # Changed from /projects to /tmp
print("--- Test 4 Passed ---")
print("\nAll Initial State and basic operations tests passed.")

126
test_script_1_mocked_v2.py Normal file
View File

@ -0,0 +1,126 @@
import os
import sys
import unittest.mock as mock
# Mock easygui and its problematic dependencies
sys.modules['easygui'] = mock.MagicMock()
sys.modules['tkinter'] = mock.MagicMock()
sys.modules['Tkinter'] = mock.MagicMock()
mock_easygui = mock.MagicMock()
mock_easygui.msgbox = mock.MagicMock()
mock_easygui.ynbox = mock.MagicMock()
sys.modules['easygui'] = mock_easygui
os.environ["COLAB_GPU"] = "True" # Simulate non-interactive environment
# Check if config.toml exists before KohyaSSGUIConfig is initialized
CONFIG_PATH = "/app/config.toml"
if os.path.exists(CONFIG_PATH):
print(f"WARNING: {CONFIG_PATH} exists before test! Content:")
with open(CONFIG_PATH, "r") as f:
print(f.read())
# Attempt to remove it again just in case
os.remove(CONFIG_PATH)
print(f"WARNING: Removed pre-existing {CONFIG_PATH}")
else:
print(f"{CONFIG_PATH} does not exist before test, as expected.")
from kohya_gui.class_gui_config import KohyaSSGUIConfig
from kohya_gui.common_gui import get_folder_path, scriptdir, get_file_path, get_saveasfilename_path
print(f"Scriptdir resolved to: {scriptdir}")
# Ensure scriptdir is an absolute path for consistency
if not os.path.isabs(scriptdir): # This should not happen if common_gui.py defines it as absolute
scriptdir = os.path.abspath(scriptdir) # Make it absolute for safety in test logic
print(f"Updated scriptdir to absolute path: {scriptdir}")
# --- Test 1: Initial state and first folder operation ---
print("--- Test 1: Initial Folder Operation ---")
config_handler = KohyaSSGUIConfig(config_file_path=CONFIG_PATH)
initial_last_folder = config_handler.get_last_used_folder()
print(f"Initial last_used_folder from new config object: {initial_last_folder}")
assert initial_last_folder == scriptdir, f"Expected {scriptdir}, got {initial_last_folder}"
returned_path = get_folder_path(folder_path="/tmp/test_folder1", config=config_handler)
print(f"Returned path from get_folder_path: {returned_path}")
assert returned_path == "/tmp/test_folder1"
current_last_folder = config_handler.get_last_used_folder()
print(f"Last used folder after get_folder_path: {current_last_folder}")
assert current_last_folder == "/tmp/test_folder1", f"Expected /tmp/test_folder1, got {current_last_folder}"
config_handler.save_config(config=config_handler.config, config_file_path=CONFIG_PATH)
print("Config saved.")
with open(CONFIG_PATH, "r") as f:
content = f.read()
print(f"config.toml content:\n{content}")
assert 'last_used_folder = "/tmp/test_folder1"' in content
print("--- Test 1 Passed ---")
# --- Test 2: Relaunch and second folder operation ---
print("\n--- Test 2: Relaunch and Second Folder Operation ---")
config_handler_relaunch = KohyaSSGUIConfig(config_file_path=CONFIG_PATH)
relaunch_last_folder = config_handler_relaunch.get_last_used_folder()
print(f"On relaunch, last_used_folder is: {relaunch_last_folder}")
assert relaunch_last_folder == "/tmp/test_folder1", f"Expected /tmp/test_folder1, got {relaunch_last_folder}"
returned_path_2 = get_folder_path(folder_path="/tmp/test_folder2", config=config_handler_relaunch)
print(f"Returned path from second get_folder_path: {returned_path_2}")
assert returned_path_2 == "/tmp/test_folder2"
current_last_folder_2 = config_handler_relaunch.get_last_used_folder()
print(f"Last used folder after second get_folder_path: {current_last_folder_2}")
assert current_last_folder_2 == "/tmp/test_folder2", f"Expected /tmp/test_folder2, got {current_last_folder_2}"
config_handler_relaunch.save_config(config=config_handler_relaunch.config, config_file_path=CONFIG_PATH)
print("Config saved for second operation.")
with open(CONFIG_PATH, "r") as f:
content_2 = f.read()
print(f"config.toml content after second operation:\n{content_2}")
assert 'last_used_folder = "/tmp/test_folder2"' in content_2
print("--- Test 2 Passed ---")
print("\nInitial State Check (Folders) successful.")
# --- Test 3: File open operation ---
print("\n--- Test 3: File Open Operation ---")
os.makedirs("/tmp/test_config_folder", exist_ok=True)
dummy_file_path = "/tmp/test_config_folder/my_config.json"
if not os.path.exists(dummy_file_path):
with open(dummy_file_path, "w") as f: f.write("{}")
returned_file_path = get_file_path(file_path=dummy_file_path, config=config_handler_relaunch)
print(f"Returned path from get_file_path: {returned_file_path}")
assert returned_file_path == dummy_file_path
current_last_folder_3 = config_handler_relaunch.get_last_used_folder()
print(f"Last used folder after get_file_path: {current_last_folder_3}")
assert current_last_folder_3 == "/tmp/test_config_folder", f"Expected /tmp/test_config_folder, got {current_last_folder_3}"
config_handler_relaunch.save_config(config=config_handler_relaunch.config, config_file_path=CONFIG_PATH)
with open(CONFIG_PATH, "r") as f:
content_3 = f.read()
print(f"config.toml content after file open:\n{content_3}")
assert 'last_used_folder = "/tmp/test_config_folder"' in content_3
print("--- Test 3 Passed ---")
# --- Test 4: File save operation ---
print("\n--- Test 4: File Save Operation ---")
os.makedirs("/tmp/another_save_folder", exist_ok=True)
save_file_as_path = "/tmp/another_save_folder/my_new_config.json"
returned_save_path = get_saveasfilename_path(file_path=save_file_as_path, config=config_handler_relaunch)
print(f"Returned path from get_saveasfilename_path: {returned_save_path}")
assert returned_save_path == save_file_as_path
current_last_folder_4 = config_handler_relaunch.get_last_used_folder()
print(f"Last used folder after get_saveasfilename_path: {current_last_folder_4}")
assert current_last_folder_4 == "/tmp/another_save_folder", f"Expected /tmp/another_save_folder, got {current_last_folder_4}"
config_handler_relaunch.save_config(config=config_handler_relaunch.config, config_file_path=CONFIG_PATH)
with open(CONFIG_PATH, "r") as f:
content_4 = f.read()
print(f"config.toml content after file save:\n{content_4}")
assert 'last_used_folder = "/tmp/another_save_folder"' in content_4
print("--- Test 4 Passed ---")
print("\nAll Initial State and basic operations tests passed.")

73
test_script_1_modified.py Normal file
View File

@ -0,0 +1,73 @@
import os
# Set an env var to simulate a non-interactive environment
os.environ["COLAB_GPU"] = "True"
from kohya_gui.class_gui_config import KohyaSSGUIConfig
from kohya_gui.common_gui import get_folder_path, scriptdir # scriptdir is used by get_last_used_folder as a default
# --- Test 1: Initial state and first operation ---
print("--- Test 1 ---")
# 1. Create config (simulating app launch, config file doesn't exist yet)
config_handler = KohyaSSGUIConfig(config_file_path="/app/config.toml")
# Ensure last_used_folder defaults to scriptdir (or an empty string, depending on implementation details)
# get_last_used_folder initializes it to scriptdir if not in config
print(f"Initial last_used_folder from new config object: {config_handler.get_last_used_folder()}")
# 2. Simulate get_folder_path where user "selects" /tmp/test_folder1
# In non-dialog mode, get_folder_path will set last_used_folder if folder_path is valid and config is present.
# We pass folder_path="/tmp/test_folder1" as if it was typed into a field and then an action triggered.
# Or, as if it's the path being "opened" or "selected".
returned_path = get_folder_path(folder_path="/tmp/test_folder1", config=config_handler)
print(f"Returned path from get_folder_path: {returned_path}")
print(f"Last used folder after get_folder_path: {config_handler.get_last_used_folder()}")
# 3. Save config
config_handler.save_config(config=config_handler.config, config_file_path="/app/config.toml")
print("Config saved.")
# Verify by reading the file content
with open("/app/config.toml", "r") as f:
content = f.read()
print(f"config.toml content:\n{content}")
assert 'last_used_folder = "/tmp/test_folder1"' in content
print("--- Test 1 Passed ---")
# --- Test 2: Relaunch and second operation ---
print("\n--- Test 2 ---")
# 1. Simulate relaunch (load existing config)
config_handler_relaunch = KohyaSSGUIConfig(config_file_path="/app/config.toml")
print(f"On relaunch, last_used_folder is: {config_handler_relaunch.get_last_used_folder()}")
assert config_handler_relaunch.get_last_used_folder() == "/tmp/test_folder1"
# 2. Simulate opening the same dialog again. get_folder_path will determine initial_dir.
# We are interested in what initial_dir would be.
# The common_gui.get_folder_path function has this logic:
# initial_dir_to_use = scriptdir
# if config:
# last_used = config.get_last_used_folder()
# if last_used and os.path.isdir(last_used):
# initial_dir_to_use = last_used
# So, initial_dir_to_use should become /tmp/test_folder1.
# (This part is harder to directly assert without refactoring get_folder_path to return initial_dir for testing,
# or by checking logs if we had more verbose logging for initial_dir decision)
# For now, we trust that get_last_used_folder returning /tmp/test_folder1 means the dialog would use it.
# 3. Simulate selecting another folder: /tmp/test_folder2
returned_path_2 = get_folder_path(folder_path="/tmp/test_folder2", config=config_handler_relaunch)
print(f"Returned path from second get_folder_path: {returned_path_2}")
print(f"Last used folder after second get_folder_path: {config_handler_relaunch.get_last_used_folder()}")
# 4. Save config again
config_handler_relaunch.save_config(config=config_handler_relaunch.config, config_file_path="/app/config.toml")
print("Config saved for second operation.")
# Verify by reading the file content
with open("/app/config.toml", "r") as f:
content_2 = f.read()
print(f"config.toml content after second operation:\n{content_2}")
assert 'last_used_folder = "/tmp/test_folder2"' in content_2
print("--- Test 2 Passed ---")
print("\nInitial State Check successful.")

View File

@ -0,0 +1,97 @@
import os
import sys
import unittest.mock as mock
# Mock UI elements
sys.modules['easygui'] = mock.MagicMock()
sys.modules['tkinter'] = mock.MagicMock()
sys.modules['Tkinter'] = mock.MagicMock()
mock_easygui = mock.MagicMock()
mock_easygui.msgbox = mock.MagicMock()
mock_easygui.ynbox = mock.MagicMock()
sys.modules['easygui'] = mock_easygui
os.environ["COLAB_GPU"] = "True" # Simulate non-interactive environment
from kohya_gui.class_gui_config import KohyaSSGUIConfig
from kohya_gui.common_gui import get_folder_path, scriptdir # Assuming scriptdir is /app
CONFIG_PATH = "/app/config.toml"
print(f"Scriptdir is: {scriptdir}")
# --- Test 3: Corrupted last_used_folder path ---
print("\n--- Test 3: Corrupted last_used_folder path ---")
# 1. Create config.toml with a corrupted path
with open(CONFIG_PATH, "w") as f:
f.write('last_used_folder = "this/is/not/a/valid/path"\n')
print(f"Created corrupted {CONFIG_PATH}")
# 2. Load config
config_corrupted = KohyaSSGUIConfig(config_file_path=CONFIG_PATH)
corrupted_val = config_corrupted.get_last_used_folder()
print(f"get_last_used_folder() with corrupted path returned: {corrupted_val}")
# get_last_used_folder itself just returns the value if it's a string.
assert corrupted_val == "this/is/not/a/valid/path"
# 3. Simulate a folder operation. common_gui.get_folder_path should handle invalid path gracefully.
# It should default to scriptdir because "this/is/not/a/valid/path" is not a valid directory.
# We can't directly check initial_dir_to_use without modifying common_gui,
# but we can infer it by checking that last_used_folder is NOT updated if the dialog was "cancelled" (empty input path)
# and that it IS updated if a new path is "selected".
# Simulate calling get_folder_path, where it would try to use the corrupted path, fail, and use scriptdir.
# If user then cancels (empty string for folder_path), it should not update the corrupted value.
# However, our non-dialog mode logic in get_folder_path for COLAB_GPU is:
# if folder_path and os.path.isdir(folder_path) and config: config.set_last_used_folder(folder_path)
# So, if folder_path is empty, it won't update.
returned_path_corr1 = get_folder_path(folder_path="", config=config_corrupted) # Simulate empty input or cancel
assert config_corrupted.get_last_used_folder() == "this/is/not/a/valid/path", "Corrupted path should not change on empty selection"
print("Graceful handling of corrupted path (no selection) passed.")
# 4. Simulate selecting a valid folder
returned_path_corr2 = get_folder_path(folder_path="/tmp/good_folder", config=config_corrupted)
assert returned_path_corr2 == "/tmp/good_folder"
assert config_corrupted.get_last_used_folder() == "/tmp/good_folder", "Path should update to /tmp/good_folder"
print("Graceful handling of corrupted path (new selection) passed.")
# 5. Save and verify config file
config_corrupted.save_config(config=config_corrupted.config, config_file_path=CONFIG_PATH)
with open(CONFIG_PATH, "r") as f:
content = f.read()
print(f"config.toml content after fixing corrupted path:\n{content}")
assert 'last_used_folder = "/tmp/good_folder"' in content
print("--- Test 3 Passed ---")
# --- Test 4: last_used_folder key missing ---
print("\n--- Test 4: last_used_folder key missing ---")
# 1. Create config.toml without the key (or with other keys)
with open(CONFIG_PATH, "w") as f:
f.write('another_key = "some_value"\n') # No last_used_folder
print(f"Created {CONFIG_PATH} with missing last_used_folder key.")
# 2. Load config. load_config() should add last_used_folder = scriptdir
config_missing = KohyaSSGUIConfig(config_file_path=CONFIG_PATH)
val_after_load = config_missing.get_last_used_folder()
print(f"get_last_used_folder() after loading config with missing key: {val_after_load}")
# The get_last_used_folder() itself ensures it returns scriptdir if key is missing or value is bad type,
# and load_config also initializes it.
assert val_after_load == scriptdir, f"Expected scriptdir, got {val_after_load}"
# 3. Simulate a folder operation
returned_path_miss = get_folder_path(folder_path="/tmp/another_good_folder", config=config_missing)
assert returned_path_miss == "/tmp/another_good_folder"
assert config_missing.get_last_used_folder() == "/tmp/another_good_folder"
print("Graceful handling of missing key (new selection) passed.")
# 4. Save and verify
config_missing.save_config(config=config_missing.config, config_file_path=CONFIG_PATH)
with open(CONFIG_PATH, "r") as f:
content = f.read()
print(f"config.toml content after fixing missing key:\n{content}")
assert 'last_used_folder = "/tmp/another_good_folder"' in content
assert 'another_key = "some_value"' in content # Ensure other keys are preserved
print("--- Test 4 Passed ---")
print("\nAll Edge Case tests passed.")