@@ -220,7 +220,7 @@ def description(self):
220220 """
221221 raise NotImplementedError ()
222222
223- def raise_invalid_val (self , v ):
223+ def raise_invalid_val (self , v , inds = None ):
224224 """
225225 Helper method to raise an informative exception when an invalid
226226 value is passed to the validate_coerce method.
@@ -229,16 +229,25 @@ def raise_invalid_val(self, v):
229229 ----------
230230 v :
231231 Value that was input to validate_coerce and could not be coerced
232+ inds: list of int or None (default)
233+ Indexes to display after property name. e.g. if self.plotly_name
234+ is 'prop' and inds=[2, 1] then the name in the validation error
235+ message will be 'prop[2][1]`
232236 Raises
233237 -------
234238 ValueError
235239 """
240+ name = self .plotly_name
241+ if inds :
242+ for i in inds :
243+ name += '[' + str (i ) + ']'
244+
236245 raise ValueError ("""
237246 Invalid value of type {typ} received for the '{name}' property of {pname}
238247 Received value: {v}
239248
240249{valid_clr_desc}""" .format (
241- name = self . plotly_name ,
250+ name = name ,
242251 pname = self .parent_name ,
243252 typ = type_str (v ),
244253 v = repr (v ),
@@ -1611,7 +1620,8 @@ class InfoArrayValidator(BaseValidator):
16111620 ],
16121621 "otherOpts": [
16131622 "dflt",
1614- "freeLength"
1623+ "freeLength",
1624+ "dimensions"
16151625 ]
16161626 }
16171627 """
@@ -1621,10 +1631,14 @@ def __init__(self,
16211631 parent_name ,
16221632 items ,
16231633 free_length = None ,
1634+ dimensions = None ,
16241635 ** kwargs ):
16251636 super (InfoArrayValidator , self ).__init__ (
16261637 plotly_name = plotly_name , parent_name = parent_name , ** kwargs )
1638+
16271639 self .items = items
1640+ self .dimensions = dimensions if dimensions else 1
1641+ self .free_length = free_length
16281642
16291643 # Instantiate validators for each info array element
16301644 self .item_validators = []
@@ -1637,22 +1651,87 @@ def __init__(self,
16371651 item , element_name , parent_name )
16381652 self .item_validators .append (item_validator )
16391653
1640- self .free_length = free_length
1641-
16421654 def description (self ):
1643- upto = ' up to' if self .free_length else ''
1655+
1656+ # Cases
1657+ # 1) self.items is array, self.dimensions is 1
1658+ # a) free_length=True
1659+ # b) free_length=False
1660+ # 2) self.items is array, self.dimensions is 2
1661+ # (requires free_length=True)
1662+ # 3) self.items is scalar (requires free_length=True)
1663+ # a) dimensions=1
1664+ # b) dimensions=2
1665+ #
1666+ # dimensions can be set to '1-2' to indicate the both are accepted
1667+ #
16441668 desc = """\
1645- The '{plotly_name}' property is an info array that may be specified as a
1646- list or tuple of{upto} {N} elements where:
1647- """ .format (plotly_name = self .plotly_name ,
1648- upto = upto ,
1669+ The '{plotly_name}' property is an info array that may be specified as:\
1670+ """ .format (plotly_name = self .plotly_name )
1671+
1672+ if isinstance (self .items , list ):
1673+ # ### Case 1 ###
1674+ if self .dimensions in (1 , '1-2' ):
1675+ upto = (' up to'
1676+ if self .free_length and self .dimensions == 1
1677+ else '' )
1678+ desc += """
1679+
1680+ * a list or tuple of{upto} {N} elements where:\
1681+ """ .format (upto = upto ,
16491682 N = len (self .item_validators ))
16501683
1651- for i , item_validator in enumerate (self .item_validators ):
1652- el_desc = item_validator .description ().strip ()
1653- desc = desc + """
1684+ for i , item_validator in enumerate (self .item_validators ):
1685+ el_desc = item_validator .description ().strip ()
1686+ desc = desc + """
16541687({i}) {el_desc}""" .format (i = i , el_desc = el_desc )
16551688
1689+ # ### Case 2 ###
1690+ if self .dimensions in ('1-2' , 2 ):
1691+ assert self .free_length
1692+
1693+ desc += """
1694+
1695+ * a 2D list where:"""
1696+ for i , item_validator in enumerate (self .item_validators ):
1697+ # Update name for 2d
1698+ orig_name = item_validator .plotly_name
1699+ item_validator .plotly_name = "{name}[i][{i}]" .format (
1700+ name = self .plotly_name , i = i )
1701+
1702+ el_desc = item_validator .description ().strip ()
1703+ desc = desc + """
1704+ ({i}) {el_desc}""" .format (i = i , el_desc = el_desc )
1705+ item_validator .plotly_name = orig_name
1706+ else :
1707+ # ### Case 3 ###
1708+ assert self .free_length
1709+ item_validator = self .item_validators [0 ]
1710+ orig_name = item_validator .plotly_name
1711+
1712+ if self .dimensions in (1 , '1-2' ):
1713+ item_validator .plotly_name = "{name}[i]" .format (
1714+ name = self .plotly_name )
1715+
1716+ el_desc = item_validator .description ().strip ()
1717+
1718+ desc += """
1719+ * a list of elements where:
1720+ {el_desc}
1721+ """ .format (el_desc = el_desc )
1722+
1723+ if self .dimensions in ('1-2' , 2 ):
1724+ item_validator .plotly_name = "{name}[i][j]" .format (
1725+ name = self .plotly_name )
1726+
1727+ el_desc = item_validator .description ().strip ()
1728+ desc += """
1729+ * a 2D list where:
1730+ {el_desc}
1731+ """ .format (el_desc = el_desc )
1732+
1733+ item_validator .plotly_name = orig_name
1734+
16561735 return desc
16571736
16581737 @staticmethod
@@ -1670,19 +1749,106 @@ def build_validator(validator_info, plotly_name, parent_name):
16701749 return validator_class (
16711750 plotly_name = plotly_name , parent_name = parent_name , ** kwargs )
16721751
1752+ def validate_element_with_indexed_name (self , val , validator , inds ):
1753+ """
1754+ Helper to add indexes to a validator's name, call validate_coerce on
1755+ a value, then restore the original validator name.
1756+
1757+ This makes sure that if a validation error message is raised, the
1758+ property name the user sees includes the index(es) of the offending
1759+ element.
1760+
1761+ Parameters
1762+ ----------
1763+ val:
1764+ A value to be validated
1765+ validator
1766+ A validator
1767+ inds
1768+ List of one or more non-negative integers that represent the
1769+ nested index of the value being validated
1770+ Returns
1771+ -------
1772+ val
1773+ validated value
1774+
1775+ Raises
1776+ ------
1777+ ValueError
1778+ if val fails validation
1779+ """
1780+ orig_name = validator .plotly_name
1781+ new_name = self .plotly_name
1782+ for i in inds :
1783+ new_name += '[' + str (i ) + ']'
1784+ validator .plotly_name = new_name
1785+ try :
1786+ val = validator .validate_coerce (val )
1787+ finally :
1788+ validator .plotly_name = orig_name
1789+
1790+ return val
1791+
16731792 def validate_coerce (self , v ):
16741793 if v is None :
16751794 # Pass None through
1676- pass
1795+ return None
16771796 elif not is_array (v ):
16781797 self .raise_invalid_val (v )
1798+
1799+ # Save off original v value to use in error reporting
1800+ orig_v = v
1801+
1802+ # Convert everything into nested lists
1803+ # This way we don't need to worry about nested numpy arrays
1804+ v = to_scalar_or_list (v )
1805+
1806+ is_v_2d = v and is_array (v [0 ])
1807+
1808+ if is_v_2d :
1809+ if self .dimensions == 1 :
1810+ self .raise_invalid_val (orig_v )
1811+ else : # self.dimensions is '1-2' or 2
1812+ if is_array (self .items ):
1813+ # e.g. 2D list as parcoords.dimensions.constraintrange
1814+ # check that all items are there for each nested element
1815+ for i , row in enumerate (v ):
1816+ # Check row length
1817+ if not is_array (row ) or len (row ) != len (self .items ):
1818+ self .raise_invalid_val (orig_v [i ], [i ])
1819+
1820+ for j , validator in enumerate (self .item_validators ):
1821+ row [j ] = self .validate_element_with_indexed_name (
1822+ v [i ][j ], validator , [i , j ])
1823+ else :
1824+ # e.g. 2D list as layout.grid.subplots
1825+ # check that all elements match individual validator
1826+ validator = self .item_validators [0 ]
1827+ for i , row in enumerate (v ):
1828+ if not is_array (row ):
1829+ self .raise_invalid_val (orig_v [i ], [i ])
1830+
1831+ for j , el in enumerate (row ):
1832+ row [j ] = self .validate_element_with_indexed_name (
1833+ el , validator , [i , j ])
1834+ elif v and self .dimensions == 2 :
1835+ # e.g. 1D list passed as layout.grid.subplots
1836+ self .raise_invalid_val (orig_v [0 ], [0 ])
1837+ elif not is_array (self .items ):
1838+ # e.g. 1D list passed as layout.grid.xaxes
1839+ validator = self .item_validators [0 ]
1840+ for i , el in enumerate (v ):
1841+ v [i ] = self .validate_element_with_indexed_name (
1842+ el , validator , [i ])
1843+
16791844 elif not self .free_length and len (v ) != len (self .item_validators ):
1680- self .raise_invalid_val (v )
1845+ # e.g. 3 element list as layout.xaxis.range
1846+ self .raise_invalid_val (orig_v )
16811847 elif self .free_length and len (v ) > len (self .item_validators ):
1682- self .raise_invalid_val (v )
1848+ # e.g. 4 element list as layout.updatemenu.button.args
1849+ self .raise_invalid_val (orig_v )
16831850 else :
1684- # We have an array of the correct length
1685- v = to_scalar_or_list (v )
1851+ # We have a 1D array of the correct length
16861852 for i , (el , validator ) in enumerate (zip (v , self .item_validators )):
16871853 # Validate coerce elements
16881854 v [i ] = validator .validate_coerce (el )
@@ -1693,13 +1859,28 @@ def present(self, v):
16931859 if v is None :
16941860 return None
16951861 else :
1696- # Call present on each of the item validators
1697- for i , (el , validator ) in enumerate (zip (v , self .item_validators )):
1698- # Validate coerce elements
1699- v [i ] = validator .present (el )
1862+ if (self .dimensions == 2 or
1863+ self .dimensions == '1-2' and v and is_array (v [0 ])):
17001864
1701- # Return tuple form of
1702- return tuple (v )
1865+ # 2D case
1866+ v = copy .deepcopy (v )
1867+ for row in v :
1868+ for i , (el , validator ) in enumerate (
1869+ zip (row , self .item_validators )):
1870+ row [i ] = validator .present (el )
1871+
1872+ return tuple (tuple (row ) for row in v )
1873+ else :
1874+ # 1D case
1875+ v = copy .copy (v )
1876+ # Call present on each of the item validators
1877+ for i , (el , validator ) in enumerate (
1878+ zip (v , self .item_validators )):
1879+ # Validate coerce elements
1880+ v [i ] = validator .present (el )
1881+
1882+ # Return tuple form of
1883+ return tuple (v )
17031884
17041885
17051886class LiteralValidator (BaseValidator ):
0 commit comments