From 0ac1fb5d9e99dea4872666fe0f7e01bbed0a360b Mon Sep 17 00:00:00 2001 From: Nilay <118994073+NilayYadav@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:02:26 +0530 Subject: [PATCH] CLI: fix --local-dataset being parsed as a string instead of a list (#6357) * fix CLI dataset path resolution * enhance list type check --------- Co-authored-by: Michael Han <107991372+shimmyshimmer@users.noreply.github.com> --- studio/backend/core/training/trainer.py | 3 +++ unsloth_cli/options.py | 31 +++++++++++++++++++------ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/studio/backend/core/training/trainer.py b/studio/backend/core/training/trainer.py index 529b98c652..8fd2dfdfd8 100644 --- a/studio/backend/core/training/trainer.py +++ b/studio/backend/core/training/trainer.py @@ -2231,6 +2231,9 @@ class UnslothTrainer: for dataset_file in file_paths: if os.path.isabs(dataset_file): file_path = dataset_file + elif os.path.exists(dataset_file): + # A path relative to the current working directory (CLI usage) + file_path = os.path.abspath(dataset_file) else: file_path = str(resolve_dataset_path(dataset_file)) diff --git a/unsloth_cli/options.py b/unsloth_cli/options.py index 03c7540faa..7a0433e87c 100644 --- a/unsloth_cli/options.py +++ b/unsloth_cli/options.py @@ -6,7 +6,7 @@ import functools import inspect from pathlib import Path -from typing import Any, Callable, Optional, get_args, get_origin +from typing import Any, Callable, List, Optional, get_args, get_origin import typer from pydantic import BaseModel @@ -35,8 +35,16 @@ def _is_bool_field(annotation: Any) -> bool: def _is_list_type(annotation: Any) -> bool: - """Check if type is a List.""" - return get_origin(annotation) is list + """Check if type is a List (including Optional[List[...]] and bare list).""" + unwrapped = _unwrap_optional(annotation) + return unwrapped is list or get_origin(unwrapped) is list + + +def _list_element_type(annotation: Any) -> type: + """Element type for a List field; falls back to str for complex inners.""" + args = get_args(_unwrap_optional(annotation)) + elem = args[0] if args else str + return elem if elem in (str, int, float, Path) else str def _get_python_type(annotation: Any) -> type: @@ -80,7 +88,7 @@ def add_options_from_config(config_class: type[BaseModel]) -> Callable: which will receive a dict of all CLI-provided config values. """ fields = _collect_config_fields(config_class) - field_names = {name for name, field_info in fields if not _is_list_type(field_info.annotation)} + field_names = {name for name, _field_info in fields} def decorator(func: Callable) -> Callable: sig = inspect.signature(func) @@ -95,12 +103,21 @@ def add_options_from_config(config_class: type[BaseModel]) -> Callable: if field_name in original_param_names: continue annotation = field_info.annotation - if _is_list_type(annotation): - continue - flag_name = _python_name_to_cli_flag(field_name) help_text = field_info.description or "" + if _is_list_type(annotation): + # Repeatable option: --flag a --flag b -> ["a", "b"] + default = typer.Option(None, flag_name, help = help_text) + param = inspect.Parameter( + field_name, + inspect.Parameter.POSITIONAL_OR_KEYWORD, + default = default, + annotation = Optional[List[_list_element_type(annotation)]], + ) + new_params.append(param) + continue + if _is_bool_field(annotation): default = typer.Option( None,