diff --git a/python/lsst/pex/config/convert.py b/python/lsst/pex/config/convert.py index 0baff1c..9e74f14 100644 --- a/python/lsst/pex/config/convert.py +++ b/python/lsst/pex/config/convert.py @@ -33,13 +33,71 @@ dafBase = None -def _helper(ps, prefix, dict_): +def _helper(config, ps, prefix, dict_, ext_dtype=None): + """Convert to PropertySet using types from the Config.""" + try: + fields = config._fields + except AttributeError: + # dictField.Dict will not have _fields attribute. + fields = {} + for k, v in dict_.items(): name = prefix + "." + k if prefix is not None else k if isinstance(v, dict): - _helper(ps, name, v) + field = fields.get(k, None) + + if hasattr(field, "itemtype"): + itemtype = field.itemtype + else: + itemtype = None + + if hasattr(field, "typemap"): + # ChoiceField needs special layout. + # name: Default choice + # values: {ChoiceA: {}, ChoiceB: {}} + config_choice = getattr(config, k) + # Follow the toDict code for ConfigChoiceField + if hasattr(config_choice, "multi") and config_choice.multi: + ps[f"{name}.names"] = config_choice.names + else: + ps[f"{name}.name"] = config_choice.name + for k, v in config_choice.items(): + sub_name = f"{name}.values.{k}" + _helper(config_choice[k], ps, sub_name, v.toDict()) + continue + + # Pass in the item type if we have it. + _helper(getattr(config, k), ps, name, v, ext_dtype=itemtype) elif v is not None: - ps.set(name, v) + try: + field = fields[k] + except KeyError: + dtype = ext_dtype + field = None + else: + dtype = field.dtype + + # DictField and ListField have a dtype so ask for it. + if hasattr(field, "itemtype"): + dtype = field.itemtype + + if dtype is str: + ps.setString(name, v) + elif dtype is bool: + ps.setBool(name, v) + elif dtype is int: + # User code requires that small numbers are stored in + # small integers and LongLong be reserved for larger + # ints. This means that we have to either use an + # optimized version of the logic from PropertySet + # (we know it does not exist and that it is an integer type) + # or just use the generic setter. + ps.set(name, v) + elif dtype is float: + ps.setDouble(name, v) + else: + # Fall back should not happen. + ps.set(name, v) def makePropertySet(config): @@ -66,7 +124,7 @@ def makePropertySet(config): if config is not None: ps = dafBase.PropertySet() - _helper(ps, None, config.toDict()) + _helper(config, ps, None, config.toDict()) return ps else: return None diff --git a/tests/test_Config.py b/tests/test_Config.py index ad9d332..1bc7679 100644 --- a/tests/test_Config.py +++ b/tests/test_Config.py @@ -447,6 +447,10 @@ class III(AAA): @unittest.skipIf(dafBase is None, "lsst.daf.base is required") def testConvertPropertySet(self): + # assertEqual for hierarchical dicts does not seem to sort keys + # so instead convert to JSON strings before comparing. + import json + ps = pexConfig.makePropertySet(self.simple) self.assertFalse(ps.exists("i")) self.assertEqual(ps.getScalar("f"), self.simple.f) @@ -457,6 +461,13 @@ def testConvertPropertySet(self): ps = pexConfig.makePropertySet(self.comp) self.assertEqual(ps.getScalar("c.f"), self.comp.c.f) + # Check property set matches the dict. + # Have to remove the None item from the dict. + config_dict = self.comp.toDict() + for k in ("p", "r"): + del config_dict[k]["values"]["AAA"]["i"] + self.assertEqual(json.dumps(ps.toDict(), sort_keys=True), json.dumps(config_dict, sort_keys=True)) + def testFreeze(self): self.comp.freeze()