@@ -1601,6 +1601,342 @@ def test_invalid_assignment_static(self):
16011601 self .assertEqual (obj .static_plain , "INVALID" )
16021602
16031603
1604+ class TestModelOptions (kp .EnumParameterOptions ):
1605+ """Test enum for visible_choices testing"""
1606+
1607+ LINEAR = ("Linear Regression" , "Fits a linear model" )
1608+ RANDOM_FOREST = ("Random Forest" , "Ensemble tree model" )
1609+ NEURAL_NET = ("Neural Network" , "Deep learning model" )
1610+ SVM = ("Support Vector Machine" , "SVM model" )
1611+
1612+
1613+ class ParameterizedWithVisibleChoices :
1614+ """Test class for EnumParameter with visible_choices callable"""
1615+
1616+ @staticmethod
1617+ def _filter_to_two (ctx ):
1618+ """Filter to only LINEAR and RANDOM_FOREST"""
1619+ return [TestModelOptions .LINEAR , TestModelOptions .RANDOM_FOREST ]
1620+
1621+ @staticmethod
1622+ def _filter_by_specs (ctx ):
1623+ """Filter based on context specs"""
1624+ if ctx is None :
1625+ # No context - return subset (subsetting use-case)
1626+ return [TestModelOptions .LINEAR , TestModelOptions .SVM ]
1627+
1628+ specs = ctx .get_input_specs ()
1629+ if not specs or len (specs ) == 0 :
1630+ return list (TestModelOptions )
1631+
1632+ # Simulate filtering based on spec (e.g., spec has 'supported_models' attribute)
1633+ # For testing, we'll use spec count as a proxy
1634+ if len (specs ) == 1 :
1635+ return [TestModelOptions .LINEAR , TestModelOptions .RANDOM_FOREST ]
1636+ else :
1637+ return [TestModelOptions .NEURAL_NET , TestModelOptions .SVM ]
1638+
1639+ @staticmethod
1640+ def _filter_invalid ():
1641+ """Returns invalid members for testing warnings"""
1642+ return ["INVALID_OPTION" , TestModelOptions .LINEAR ]
1643+
1644+ @staticmethod
1645+ def _filter_empty (ctx ):
1646+ """Returns empty list"""
1647+ return []
1648+
1649+ param_filtered = kp .EnumParameter (
1650+ label = "Filtered Model" ,
1651+ description = "Model with filtered choices" ,
1652+ default_value = TestModelOptions .LINEAR .name ,
1653+ enum = TestModelOptions ,
1654+ visible_choices = _filter_to_two .__func__ ,
1655+ )
1656+
1657+ param_context_dependent = kp .EnumParameter (
1658+ label = "Context Dependent" ,
1659+ description = "Choices depend on context" ,
1660+ default_value = TestModelOptions .LINEAR , # Using enum member as default
1661+ enum = TestModelOptions ,
1662+ visible_choices = _filter_by_specs .__func__ ,
1663+ )
1664+
1665+ param_no_filter = kp .EnumParameter (
1666+ label = "No Filter" ,
1667+ description = "All options visible" ,
1668+ default_value = TestModelOptions .LINEAR .name ,
1669+ enum = TestModelOptions ,
1670+ )
1671+
1672+
1673+ class TestEnumParameterVisibleChoices (unittest .TestCase ):
1674+ """Test EnumParameter visible_choices functionality"""
1675+
1676+ def test_filtered_schema_contains_subset (self ):
1677+ """Test that filtered options appear in schema oneOf"""
1678+ obj = ParameterizedWithVisibleChoices ()
1679+ schema = kp .extract_schema (
1680+ obj , dialog_creation_context = DummyDialogCreationContext ()
1681+ )
1682+ s = schema ["properties" ]["model" ]["properties" ]["param_filtered" ]
1683+
1684+ self .assertIn ("oneOf" , s )
1685+ values = {entry ["const" ] for entry in s ["oneOf" ]}
1686+ # Should only contain LINEAR and RANDOM_FOREST
1687+ self .assertEqual (values , {"LINEAR" , "RANDOM_FOREST" })
1688+ self .assertNotIn ("NEURAL_NET" , values )
1689+ self .assertNotIn ("SVM" , values )
1690+
1691+ def test_no_filter_shows_all_options (self ):
1692+ """Test that parameter without visible_choices shows all options"""
1693+ obj = ParameterizedWithVisibleChoices ()
1694+ schema = kp .extract_schema (
1695+ obj , dialog_creation_context = DummyDialogCreationContext ()
1696+ )
1697+ s = schema ["properties" ]["model" ]["properties" ]["param_no_filter" ]
1698+
1699+ self .assertIn ("oneOf" , s )
1700+ values = {entry ["const" ] for entry in s ["oneOf" ]}
1701+ self .assertEqual (values , {"LINEAR" , "RANDOM_FOREST" , "NEURAL_NET" , "SVM" })
1702+
1703+ def test_context_dependent_filtering (self ):
1704+ """Test that filtering works based on context"""
1705+ obj = ParameterizedWithVisibleChoices ()
1706+
1707+ # With one spec
1708+ ctx_one = DummyDialogCreationContext (specs = [test_schema ])
1709+ schema = kp .extract_schema (obj , dialog_creation_context = ctx_one )
1710+ s = schema ["properties" ]["model" ]["properties" ]["param_context_dependent" ]
1711+ values = {entry ["const" ] for entry in s ["oneOf" ]}
1712+ self .assertEqual (values , {"LINEAR" , "RANDOM_FOREST" })
1713+
1714+ # With two specs
1715+ ctx_two = DummyDialogCreationContext (specs = [test_schema , test_schema ])
1716+ schema = kp .extract_schema (obj , dialog_creation_context = ctx_two )
1717+ s = schema ["properties" ]["model" ]["properties" ]["param_context_dependent" ]
1718+ values = {entry ["const" ] for entry in s ["oneOf" ]}
1719+ self .assertEqual (values , {"NEURAL_NET" , "SVM" })
1720+
1721+ def test_none_context_subsetting (self ):
1722+ """Test that None context enables subsetting use-case"""
1723+ obj = ParameterizedWithVisibleChoices ()
1724+
1725+ # Extract schema without context
1726+ schema = kp .extract_schema (obj , dialog_creation_context = None )
1727+ s = schema ["properties" ]["model" ]["properties" ]["param_context_dependent" ]
1728+
1729+ self .assertIn ("oneOf" , s )
1730+ values = {entry ["const" ] for entry in s ["oneOf" ]}
1731+ # Should return subset defined for None case
1732+ self .assertEqual (values , {"LINEAR" , "SVM" })
1733+
1734+ def test_description_respects_visible_choices (self ):
1735+ """Test that description respects visible_choices based on None context"""
1736+
1737+ # param_filtered uses _filter_to_two which doesn't check context
1738+ # So description should show only LINEAR and RANDOM_FOREST
1739+ desc_dict = ParameterizedWithVisibleChoices .param_filtered ._extract_description (
1740+ "param_filtered" , None
1741+ )
1742+ description = desc_dict ["description" ]
1743+
1744+ # Description should contain only filtered options
1745+ self .assertIn ("Linear Regression" , description )
1746+ self .assertIn ("Random Forest" , description )
1747+ # Should NOT contain filtered-out options
1748+ self .assertNotIn ("Neural Network" , description )
1749+ self .assertNotIn ("Support Vector Machine" , description )
1750+
1751+ def test_description_with_context_dependent_filter (self ):
1752+ """Test description with context-dependent filter (None case)"""
1753+
1754+ # param_context_dependent returns subset for None context
1755+ desc_dict = ParameterizedWithVisibleChoices .param_context_dependent ._extract_description (
1756+ "param_context_dependent" , None
1757+ )
1758+ description = desc_dict ["description" ]
1759+
1760+ # Description should show subset returned for None
1761+ self .assertIn ("Linear Regression" , description )
1762+ self .assertIn ("Support Vector Machine" , description )
1763+ # Should NOT contain other options
1764+ self .assertNotIn ("Random Forest" , description )
1765+ self .assertNotIn ("Neural Network" , description )
1766+
1767+ def test_description_without_filter_shows_all (self ):
1768+ """Test that description without filter shows all options"""
1769+
1770+ # param_no_filter has no visible_choices
1771+ desc_dict = (
1772+ ParameterizedWithVisibleChoices .param_no_filter ._extract_description (
1773+ "param_no_filter" , None
1774+ )
1775+ )
1776+ description = desc_dict ["description" ]
1777+
1778+ # Description should contain all enum options
1779+ self .assertIn ("Linear Regression" , description )
1780+ self .assertIn ("Random Forest" , description )
1781+ self .assertIn ("Neural Network" , description )
1782+ self .assertIn ("Support Vector Machine" , description )
1783+
1784+ def test_validation_accepts_filtered_out_values (self ):
1785+ """Test that validation accepts any enum member, even if filtered out"""
1786+ obj = ParameterizedWithVisibleChoices ()
1787+
1788+ # Extract schema with filtering active
1789+ kp .extract_schema (obj , dialog_creation_context = DummyDialogCreationContext ())
1790+
1791+ # NEURAL_NET is filtered out but should still be valid
1792+ obj .param_filtered = "NEURAL_NET"
1793+ self .assertEqual (obj .param_filtered , "NEURAL_NET" )
1794+
1795+ # Invalid value should still fail validation
1796+ with self .assertRaises (ValueError ):
1797+ obj .param_filtered = "INVALID_OPTION"
1798+
1799+ def test_default_as_enum_member (self ):
1800+ """Test that default_value accepts enum member directly"""
1801+ obj = ParameterizedWithVisibleChoices ()
1802+
1803+ # param_context_dependent uses enum member as default
1804+ self .assertEqual (obj .param_context_dependent , "LINEAR" )
1805+
1806+ def test_empty_filter_result_shows_empty (self ):
1807+ """Test that empty filter result shows no options with warning"""
1808+
1809+ def empty_filter (ctx ):
1810+ return []
1811+
1812+ param = kp .EnumParameter (
1813+ label = "Empty Filter" ,
1814+ description = "Should be empty" ,
1815+ default_value = TestModelOptions .LINEAR .name ,
1816+ enum = TestModelOptions ,
1817+ visible_choices = empty_filter ,
1818+ )
1819+
1820+ class TestObj :
1821+ empty_param = param
1822+
1823+ obj = TestObj ()
1824+
1825+ # Should log warning and return empty
1826+ with self .assertLogs ("Python backend" , level = "WARNING" ) as log :
1827+ schema = kp .extract_schema (
1828+ obj , dialog_creation_context = DummyDialogCreationContext ()
1829+ )
1830+
1831+ # Check warning was logged
1832+ self .assertTrue (
1833+ any (
1834+ "returned an empty list" in msg or "empty options" in msg
1835+ for msg in log .output
1836+ )
1837+ )
1838+
1839+ s = schema ["properties" ]["model" ]["properties" ]["empty_param" ]
1840+ self .assertEqual (s ["oneOf" ], [])
1841+
1842+ def test_invalid_members_filtered_with_warning (self ):
1843+ """Test that invalid members are filtered out with warning"""
1844+
1845+ def invalid_filter (ctx ):
1846+ # Return mix of valid and invalid
1847+ class FakeMember :
1848+ name = "INVALID"
1849+
1850+ return [TestModelOptions .LINEAR , FakeMember (), "not_a_member" ]
1851+
1852+ param = kp .EnumParameter (
1853+ label = "Invalid Filter" ,
1854+ description = "Has invalid members" ,
1855+ default_value = TestModelOptions .LINEAR .name ,
1856+ enum = TestModelOptions ,
1857+ visible_choices = invalid_filter ,
1858+ )
1859+
1860+ class TestObj :
1861+ invalid_param = param
1862+
1863+ obj = TestObj ()
1864+
1865+ # Should log warning about invalid members
1866+ with self .assertLogs ("Python backend" , level = "WARNING" ) as log :
1867+ schema = kp .extract_schema (
1868+ obj , dialog_creation_context = DummyDialogCreationContext ()
1869+ )
1870+
1871+ # Check warning was logged with valid options listed
1872+ warning_msg = " " .join (log .output )
1873+ self .assertIn ("invalid members" , warning_msg .lower ())
1874+ self .assertIn ("Valid options" , warning_msg )
1875+
1876+ # Schema should only contain valid member
1877+ s = schema ["properties" ]["model" ]["properties" ]["invalid_param" ]
1878+ values = {entry ["const" ] for entry in s ["oneOf" ]}
1879+ self .assertEqual (values , {"LINEAR" })
1880+
1881+ def test_default_not_in_visible_options_warns (self ):
1882+ """Test that warning is logged when default is not in visible options"""
1883+
1884+ def filter_without_default (ctx ):
1885+ return [TestModelOptions .RANDOM_FOREST , TestModelOptions .SVM ]
1886+
1887+ param = kp .EnumParameter (
1888+ label = "Default Not Visible" ,
1889+ description = "Default filtered out" ,
1890+ default_value = TestModelOptions .LINEAR .name , # Not in visible choices
1891+ enum = TestModelOptions ,
1892+ visible_choices = filter_without_default ,
1893+ )
1894+
1895+ class TestObj :
1896+ param_with_hidden_default = param
1897+
1898+ obj = TestObj ()
1899+
1900+ # Should log warning about default not visible
1901+ with self .assertLogs ("Python backend" , level = "WARNING" ) as log :
1902+ kp .extract_schema (obj , dialog_creation_context = DummyDialogCreationContext ())
1903+
1904+ warning_msg = " " .join (log .output )
1905+ self .assertIn ("Default value" , warning_msg )
1906+ self .assertIn ("not in the currently visible options" , warning_msg )
1907+
1908+ def test_caching_works (self ):
1909+ """Test that visible_choices callable is cached per context"""
1910+ call_count = [0 ]
1911+
1912+ def counting_filter (ctx ):
1913+ call_count [0 ] += 1
1914+ return [TestModelOptions .LINEAR , TestModelOptions .SVM ]
1915+
1916+ param = kp .EnumParameter (
1917+ label = "Cached" ,
1918+ description = "Should cache" ,
1919+ default_value = TestModelOptions .LINEAR .name ,
1920+ enum = TestModelOptions ,
1921+ visible_choices = counting_filter ,
1922+ )
1923+
1924+ class TestObj :
1925+ cached_param = param
1926+
1927+ obj = TestObj ()
1928+ ctx = DummyDialogCreationContext ()
1929+
1930+ # Extract schema multiple times with same context
1931+ kp .extract_schema (obj , dialog_creation_context = ctx )
1932+ kp .extract_schema (obj , dialog_creation_context = ctx )
1933+ kp .extract_schema (obj , dialog_creation_context = ctx )
1934+
1935+ # Should be called once: the same context is used for both description and schema
1936+ # generation, and the result is cached after the first call
1937+ self .assertEqual (call_count [0 ], 1 )
1938+
1939+
16041940class DummyDialogCreationContext :
16051941 def __init__ (self , specs : List = None ) -> None :
16061942 class DummyJavaContext :
0 commit comments