diff --git a/python/lsst/pex/config/listField.py b/python/lsst/pex/config/listField.py index 530728f..4a02d56 100644 --- a/python/lsst/pex/config/listField.py +++ b/python/lsst/pex/config/listField.py @@ -294,6 +294,9 @@ class ListField(Field[List[FieldTypeVar]], Generic[FieldTypeVar]): deprecated : None or `str`, optional A description of why this Field is deprecated, including removal date. If not None, the string is appended to the docstring for this Field. + single : `bool`, optional + If ``single`` is `True`, a single object of ``dtype`` rather than a + list is okay. See Also -------- @@ -320,6 +323,7 @@ def __init__( minLength=None, maxLength=None, deprecated=None, + single=False, ): if dtype is None: raise ValueError( @@ -352,9 +356,13 @@ def __init__( raise ValueError("'itemCheck' must be callable") source = getStackFrame() + if single: + dtype_setup = (List,) + dtype if isinstance(dtype, tuple) else (List, dtype) + else: + dtype_setup = List self._setup( doc=doc, - dtype=List, + dtype=dtype_setup, default=default, check=None, optional=optional, @@ -390,6 +398,9 @@ def __init__( to disable checking the list's maximum length). """ + self.single = single + """Control whether a single object of dtype is okay.""" + def validate(self, instance): """Validate the field. @@ -415,7 +426,7 @@ def validate(self, instance): """ Field.validate(self, instance) value = self.__get__(instance) - if value is not None: + if not self.single and value is not None: lenValue = len(value) if self.length is not None and not lenValue == self.length: msg = "Required list length=%d, got length=%d" % (self.length, lenValue) @@ -444,7 +455,14 @@ def __set__( at = getCallStack() if value is not None: - value = List(instance, self, value, at, label) + if not self.single or isinstance(value, list | tuple): + value = List(instance, self, value, at, label) + else: + value = _autocast(value, self.dtype) + try: + self._validateValue(value) + except BaseException as e: + raise FieldValidationError(self, instance, str(e)) else: history = instance._history.setdefault(self.name, []) history.append((value, at, label)) @@ -467,7 +485,10 @@ def toDict(self, instance): Plain `list` of items, or `None` if the field is not set. """ value = self.__get__(instance) - return list(value) if value is not None else None + if isinstance(value, List): + return list(value) + else: + return value def _compare(self, instance1, instance2, shortcut, rtol, atol, output): """Compare two config instances for equality with respect to this @@ -511,9 +532,14 @@ def _compare(self, instance1, instance2, shortcut, rtol, atol, output): return False if l1 is None and l2 is None: return True + equal = True + if not isinstance(l1, List): + if not isinstance(l2, List): + return compareScalars(name, l1, l2, dtype=self.dtype[1], rtol=rtol, atol=atol, output=output) + else: + return False if not compareScalars(f"size for {name}", len(l1), len(l2), output=output): return False - equal = True for n, v1, v2 in zip(range(len(l1)), l1, l2): result = compareScalars( "%s[%d]" % (name, n), v1, v2, dtype=self.dtype, rtol=rtol, atol=atol, output=output