Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -1601,6 +1601,370 @@ def test_invalid_assignment_static(self):
self.assertEqual(obj.static_plain, "INVALID")


class TestModelOptions(kp.EnumParameterOptions):
"""Test enum for hidden_choices testing"""

LINEAR = ("Linear Regression", "Fits a linear model")
RANDOM_FOREST = ("Random Forest", "Ensemble tree model")
NEURAL_NET = ("Neural Network", "Deep learning model")
SVM = ("Support Vector Machine", "SVM model")


class ParameterizedWithHiddenChoices:
"""Test class for EnumParameter with hidden_choices callable"""

@staticmethod
def _hide_two(ctx):
"""Hide NEURAL_NET and SVM"""
return [TestModelOptions.NEURAL_NET, TestModelOptions.SVM]

@staticmethod
def _hide_by_specs(ctx):
"""Hide based on context specs"""
if ctx is None:
# No context - hide advanced option (hiding use-case)
return [TestModelOptions.NEURAL_NET, TestModelOptions.RANDOM_FOREST]

specs = ctx.get_input_specs()
if not specs or len(specs) == 0:
return [] # Hide nothing

# Simulate filtering based on spec (e.g., spec has 'supported_models' attribute)
# For testing, we'll use spec count as a proxy
if len(specs) == 1:
return [TestModelOptions.NEURAL_NET, TestModelOptions.SVM]
else:
return [TestModelOptions.LINEAR, TestModelOptions.RANDOM_FOREST]

@staticmethod
def _hide_invalid():
"""Returns invalid members for testing warnings"""
return ["INVALID_OPTION", TestModelOptions.LINEAR]

@staticmethod
def _hide_empty(ctx):
"""Returns empty list - hide nothing"""
return []

param_filtered = kp.EnumParameter(
label="Filtered Model",
description="Model with filtered choices",
default_value=TestModelOptions.LINEAR.name,
enum=TestModelOptions,
hidden_choices=_hide_two.__func__,
)

param_context_dependent = kp.EnumParameter(
label="Context Dependent",
description="Choices depend on context",
default_value=TestModelOptions.LINEAR, # Using enum member as default
enum=TestModelOptions,
hidden_choices=_hide_by_specs.__func__,
)

param_no_filter = kp.EnumParameter(
label="No Filter",
description="All options visible",
default_value=TestModelOptions.LINEAR.name,
enum=TestModelOptions,
)


class TestEnumParameterHiddenChoices(unittest.TestCase):
"""Test EnumParameter hidden_choices functionality"""

def test_filtered_schema_contains_subset(self):
"""Test that filtered options appear in schema oneOf"""
obj = ParameterizedWithHiddenChoices()
schema = kp.extract_schema(
obj, dialog_creation_context=DummyDialogCreationContext()
)
s = schema["properties"]["model"]["properties"]["param_filtered"]

self.assertIn("oneOf", s)
values = {entry["const"] for entry in s["oneOf"]}
# Should only contain LINEAR and RANDOM_FOREST (NEURAL_NET and SVM are hidden)
self.assertEqual(values, {"LINEAR", "RANDOM_FOREST"})
self.assertNotIn("NEURAL_NET", values)
self.assertNotIn("SVM", values)

def test_no_filter_shows_all_options(self):
"""Test that parameter without hidden_choices shows all options"""
obj = ParameterizedWithHiddenChoices()
schema = kp.extract_schema(
obj, dialog_creation_context=DummyDialogCreationContext()
)
s = schema["properties"]["model"]["properties"]["param_no_filter"]

self.assertIn("oneOf", s)
values = {entry["const"] for entry in s["oneOf"]}
self.assertEqual(values, {"LINEAR", "RANDOM_FOREST", "NEURAL_NET", "SVM"})

def test_context_dependent_filtering(self):
"""Test that filtering works based on context"""
obj = ParameterizedWithHiddenChoices()

# With one spec - hide NEURAL_NET and SVM
ctx_one = DummyDialogCreationContext(specs=[test_schema])
schema = kp.extract_schema(obj, dialog_creation_context=ctx_one)
s = schema["properties"]["model"]["properties"]["param_context_dependent"]
values = {entry["const"] for entry in s["oneOf"]}
self.assertEqual(values, {"LINEAR", "RANDOM_FOREST"})

# With two specs - hide LINEAR and RANDOM_FOREST
ctx_two = DummyDialogCreationContext(specs=[test_schema, test_schema])
schema = kp.extract_schema(obj, dialog_creation_context=ctx_two)
s = schema["properties"]["model"]["properties"]["param_context_dependent"]
values = {entry["const"] for entry in s["oneOf"]}
self.assertEqual(values, {"NEURAL_NET", "SVM"})

def test_none_context_hiding(self):
"""Test that None context enables hiding use-case"""
obj = ParameterizedWithHiddenChoices()

# Extract schema without context
schema = kp.extract_schema(obj, dialog_creation_context=None)
s = schema["properties"]["model"]["properties"]["param_context_dependent"]

self.assertIn("oneOf", s)
values = {entry["const"] for entry in s["oneOf"]}
# Should hide NEURAL_NET and RANDOM_FOREST (defined for None case)
self.assertEqual(values, {"LINEAR", "SVM"})

def test_description_respects_hidden_choices(self):
"""Test that description respects hidden_choices based on None context"""

# param_filtered uses _hide_two which doesn't check context
# So description should show only LINEAR and RANDOM_FOREST
desc_dict = ParameterizedWithHiddenChoices.param_filtered._extract_description(
"param_filtered", None
)
description = desc_dict["description"]

# Description should contain only non-hidden options
self.assertIn("Linear Regression", description)
self.assertIn("Random Forest", description)
# Should NOT contain hidden options
self.assertNotIn("Neural Network", description)
self.assertNotIn("Support Vector Machine", description)

def test_description_with_context_dependent_filter(self):
"""Test description with context-dependent filter (None case)"""

# param_context_dependent hides NEURAL_NET and RANDOM_FOREST for None context
desc_dict = (
ParameterizedWithHiddenChoices.param_context_dependent._extract_description(
"param_context_dependent", None
)
)
description = desc_dict["description"]

# Description should show non-hidden options
self.assertIn("Linear Regression", description)
self.assertIn("Support Vector Machine", description)
# Should NOT contain hidden options
self.assertNotIn("Random Forest", description)
self.assertNotIn("Neural Network", description)

def test_description_without_filter_shows_all(self):
"""Test that description without filter shows all options"""

# param_no_filter has no hidden_choices
desc_dict = ParameterizedWithHiddenChoices.param_no_filter._extract_description(
"param_no_filter", None
)
description = desc_dict["description"]

# Description should contain all enum options
self.assertIn("Linear Regression", description)
self.assertIn("Random Forest", description)
self.assertIn("Neural Network", description)
self.assertIn("Support Vector Machine", description)

def test_validation_accepts_filtered_out_values(self):
"""Test that validation accepts any enum member, even if filtered out"""
obj = ParameterizedWithHiddenChoices()

# Extract schema with filtering active
kp.extract_schema(obj, dialog_creation_context=DummyDialogCreationContext())

# NEURAL_NET is hidden but should still be valid
obj.param_filtered = "NEURAL_NET"
self.assertEqual(obj.param_filtered, "NEURAL_NET")

# Invalid value should still fail validation
with self.assertRaises(ValueError):
obj.param_filtered = "INVALID_OPTION"

def test_default_as_enum_member(self):
"""Test that default_value accepts enum member directly"""
obj = ParameterizedWithHiddenChoices()

# param_context_dependent uses enum member as default
self.assertEqual(obj.param_context_dependent, "LINEAR")

def test_empty_filter_result_shows_all(self):
"""Test that empty filter result shows all options (hide nothing)"""

def empty_filter(ctx):
return []

param = kp.EnumParameter(
label="Empty Filter",
description="Should show all",
default_value=TestModelOptions.LINEAR.name,
enum=TestModelOptions,
hidden_choices=empty_filter,
)

class TestObj:
empty_param = param

obj = TestObj()

# Should show all options (no warning expected for empty hide list)
schema = kp.extract_schema(
obj, dialog_creation_context=DummyDialogCreationContext()
)

s = schema["properties"]["model"]["properties"]["empty_param"]
values = {entry["const"] for entry in s["oneOf"]}
# Should show all options
self.assertEqual(values, {"LINEAR", "RANDOM_FOREST", "NEURAL_NET", "SVM"})

def test_invalid_members_filtered_with_warning(self):
"""Test that invalid members are filtered out with warning"""

def invalid_filter(ctx):
# Return mix of valid and invalid
class FakeMember:
name = "INVALID"

return [TestModelOptions.LINEAR, FakeMember(), "not_a_member"]

param = kp.EnumParameter(
label="Invalid Filter",
description="Has invalid members",
default_value=TestModelOptions.LINEAR.name,
enum=TestModelOptions,
hidden_choices=invalid_filter,
)

class TestObj:
invalid_param = param

obj = TestObj()

# Should log warning about invalid members
with self.assertLogs("Python backend", level="WARNING") as log:
schema = kp.extract_schema(
obj, dialog_creation_context=DummyDialogCreationContext()
)

# Check warning was logged with valid options listed
warning_msg = " ".join(log.output)
self.assertIn("invalid members", warning_msg.lower())
self.assertIn("Valid options", warning_msg)

# Schema should hide only LINEAR (which is valid, but the only valid member in the hidden list)
# The invalid members in the list are filtered out
s = schema["properties"]["model"]["properties"]["invalid_param"]
values = {entry["const"] for entry in s["oneOf"]}
# LINEAR is hidden, others remain visible
self.assertEqual(values, {"RANDOM_FOREST", "NEURAL_NET", "SVM"})

def test_default_not_in_visible_options_warns(self):
"""Test that warning is logged when default is not in visible options"""

def hide_including_default(ctx):
# Hide LINEAR (which is the default) and NEURAL_NET
return [TestModelOptions.LINEAR, TestModelOptions.NEURAL_NET]

param = kp.EnumParameter(
label="Default Not Visible",
description="Default hidden",
default_value=TestModelOptions.LINEAR.name, # Will be hidden
enum=TestModelOptions,
hidden_choices=hide_including_default,
)

class TestObj:
param_with_hidden_default = param

obj = TestObj()

# Should log warning about default not visible
with self.assertLogs("Python backend", level="WARNING") as log:
kp.extract_schema(obj, dialog_creation_context=DummyDialogCreationContext())

warning_msg = " ".join(log.output)
self.assertIn("Default value", warning_msg)
self.assertIn("not in the currently visible options", warning_msg)

def test_caching_works(self):
"""Test that hidden_choices callable is cached per context"""
call_count = [0]

def counting_filter(ctx):
call_count[0] += 1
return [TestModelOptions.NEURAL_NET, TestModelOptions.SVM]

param = kp.EnumParameter(
label="Cached",
description="Should cache",
default_value=TestModelOptions.LINEAR.name,
enum=TestModelOptions,
hidden_choices=counting_filter,
)

class TestObj:
cached_param = param

obj = TestObj()
ctx = DummyDialogCreationContext()

# Extract schema multiple times with same context
kp.extract_schema(obj, dialog_creation_context=ctx)
kp.extract_schema(obj, dialog_creation_context=ctx)
kp.extract_schema(obj, dialog_creation_context=ctx)

# For a given dialog_creation_context, the callable should be invoked only once;
# subsequent uses with the same context reuse the cached result.
self.assertEqual(call_count[0], 1)

def test_hiding_all_options_shows_empty(self):
"""Test that hiding all options results in empty list with warning"""

def hide_all(ctx):
return list(TestModelOptions)

param = kp.EnumParameter(
label="Hide All",
description="All hidden",
default_value=TestModelOptions.LINEAR.name,
enum=TestModelOptions,
hidden_choices=hide_all,
)

class TestObj:
hide_all_param = param

obj = TestObj()

# Should log warning about hiding all options
with self.assertLogs("Python backend", level="WARNING") as log:
schema = kp.extract_schema(
obj, dialog_creation_context=DummyDialogCreationContext()
)

# Check warning was logged
warning_msg = " ".join(log.output)
self.assertIn("would hide all options", warning_msg.lower())

s = schema["properties"]["model"]["properties"]["hide_all_param"]
self.assertEqual(s["oneOf"], [])


class DummyDialogCreationContext:
def __init__(self, specs: List = None) -> None:
class DummyJavaContext:
Expand Down
Loading