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