lsst-pex-config 27.2024.3500__py3-none-any.whl → 29.2025.4900__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lsst/pex/config/callStack.py +2 -2
- lsst/pex/config/comparison.py +11 -7
- lsst/pex/config/config.py +58 -20
- lsst/pex/config/configChoiceField.py +43 -28
- lsst/pex/config/configDictField.py +65 -10
- lsst/pex/config/configField.py +7 -0
- lsst/pex/config/configurableActions/_configurableAction.py +6 -1
- lsst/pex/config/configurableActions/_configurableActionStructField.py +19 -6
- lsst/pex/config/configurableActions/tests.py +1 -1
- lsst/pex/config/configurableField.py +27 -10
- lsst/pex/config/dictField.py +56 -27
- lsst/pex/config/history.py +14 -11
- lsst/pex/config/listField.py +46 -27
- lsst/pex/config/registry.py +3 -3
- lsst/pex/config/version.py +1 -1
- lsst/pex/config/wrap.py +6 -4
- {lsst_pex_config-27.2024.3500.dist-info → lsst_pex_config-29.2025.4900.dist-info}/METADATA +9 -9
- lsst_pex_config-29.2025.4900.dist-info/RECORD +35 -0
- {lsst_pex_config-27.2024.3500.dist-info → lsst_pex_config-29.2025.4900.dist-info}/WHEEL +1 -1
- lsst_pex_config-27.2024.3500.dist-info/RECORD +0 -35
- {lsst_pex_config-27.2024.3500.dist-info → lsst_pex_config-29.2025.4900.dist-info/licenses}/COPYRIGHT +0 -0
- {lsst_pex_config-27.2024.3500.dist-info → lsst_pex_config-29.2025.4900.dist-info/licenses}/LICENSE +0 -0
- {lsst_pex_config-27.2024.3500.dist-info → lsst_pex_config-29.2025.4900.dist-info/licenses}/bsd_license.txt +0 -0
- {lsst_pex_config-27.2024.3500.dist-info → lsst_pex_config-29.2025.4900.dist-info/licenses}/gpl-v3.0.txt +0 -0
- {lsst_pex_config-27.2024.3500.dist-info → lsst_pex_config-29.2025.4900.dist-info}/top_level.txt +0 -0
- {lsst_pex_config-27.2024.3500.dist-info → lsst_pex_config-29.2025.4900.dist-info}/zip-safe +0 -0
lsst/pex/config/callStack.py
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
# You should have received a copy of the GNU General Public License
|
|
26
26
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
27
27
|
|
|
28
|
-
__all__ = ["
|
|
28
|
+
__all__ = ["StackFrame", "getCallStack", "getCallerFrame", "getStackFrame"]
|
|
29
29
|
|
|
30
30
|
import inspect
|
|
31
31
|
import linecache
|
|
@@ -50,7 +50,7 @@ def getCallerFrame(relative=0):
|
|
|
50
50
|
This function is excluded from the frame.
|
|
51
51
|
"""
|
|
52
52
|
frame = inspect.currentframe().f_back.f_back # Our caller's caller
|
|
53
|
-
for
|
|
53
|
+
for _ in range(relative):
|
|
54
54
|
frame = frame.f_back
|
|
55
55
|
return frame
|
|
56
56
|
|
lsst/pex/config/comparison.py
CHANGED
|
@@ -32,7 +32,7 @@ or `lsst.pex.config.Field._compare` implementation, as they take care of
|
|
|
32
32
|
writing messages as well as floating-point comparisons and shortcuts.
|
|
33
33
|
"""
|
|
34
34
|
|
|
35
|
-
__all__ = ("
|
|
35
|
+
__all__ = ("compareConfigs", "compareScalars", "getComparisonName")
|
|
36
36
|
|
|
37
37
|
import numpy
|
|
38
38
|
|
|
@@ -68,7 +68,8 @@ def compareScalars(name, v1, v2, output, rtol=1e-8, atol=1e-8, dtype=None):
|
|
|
68
68
|
----------
|
|
69
69
|
name : `str`
|
|
70
70
|
Name to use when reporting differences, typically created by
|
|
71
|
-
`getComparisonName`.
|
|
71
|
+
`getComparisonName`. This will always appear as the beginning of any
|
|
72
|
+
messages reported via ``output``.
|
|
72
73
|
v1 : object
|
|
73
74
|
Left-hand side value to compare.
|
|
74
75
|
v2 : object
|
|
@@ -104,7 +105,7 @@ def compareScalars(name, v1, v2, output, rtol=1e-8, atol=1e-8, dtype=None):
|
|
|
104
105
|
else:
|
|
105
106
|
result = v1 == v2
|
|
106
107
|
if not result and output is not None:
|
|
107
|
-
output(f"
|
|
108
|
+
output(f"{name}: {v1!r} != {v2!r}")
|
|
108
109
|
return result
|
|
109
110
|
|
|
110
111
|
|
|
@@ -117,7 +118,8 @@ def compareConfigs(name, c1, c2, shortcut=True, rtol=1e-8, atol=1e-8, output=Non
|
|
|
117
118
|
----------
|
|
118
119
|
name : `str`
|
|
119
120
|
Name to use when reporting differences, typically created by
|
|
120
|
-
`getComparisonName`.
|
|
121
|
+
`getComparisonName`. This will always appear as the beginning of any
|
|
122
|
+
messages reported via ``output``.
|
|
121
123
|
c1 : `lsst.pex.config.Config`
|
|
122
124
|
Left-hand side config to compare.
|
|
123
125
|
c2 : `lsst.pex.config.Config`
|
|
@@ -150,22 +152,24 @@ def compareConfigs(name, c1, c2, shortcut=True, rtol=1e-8, atol=1e-8, output=Non
|
|
|
150
152
|
`~lsst.pex.config.ConfigChoiceField` instances, *unselected*
|
|
151
153
|
`~lsst.pex.config.Config` instances will not be compared.
|
|
152
154
|
"""
|
|
155
|
+
from .config import _typeStr
|
|
156
|
+
|
|
153
157
|
assert name is not None
|
|
154
158
|
if c1 is None:
|
|
155
159
|
if c2 is None:
|
|
156
160
|
return True
|
|
157
161
|
else:
|
|
158
162
|
if output is not None:
|
|
159
|
-
output(f"
|
|
163
|
+
output(f"{name}: None != {c2!r}.")
|
|
160
164
|
return False
|
|
161
165
|
else:
|
|
162
166
|
if c2 is None:
|
|
163
167
|
if output is not None:
|
|
164
|
-
output(f"
|
|
168
|
+
output(f"{name}: {c1!r} != None.")
|
|
165
169
|
return False
|
|
166
170
|
if type(c1) is not type(c2):
|
|
167
171
|
if output is not None:
|
|
168
|
-
output(f"
|
|
172
|
+
output(f"{name}: config types do not match; {_typeStr(c1)} != {_typeStr(c2)}.")
|
|
169
173
|
return False
|
|
170
174
|
equal = True
|
|
171
175
|
for field in c1._fields.values():
|
lsst/pex/config/config.py
CHANGED
|
@@ -30,15 +30,16 @@ __all__ = (
|
|
|
30
30
|
"Config",
|
|
31
31
|
"ConfigMeta",
|
|
32
32
|
"Field",
|
|
33
|
+
"FieldTypeVar",
|
|
33
34
|
"FieldValidationError",
|
|
34
35
|
"UnexpectedProxyUsageError",
|
|
35
|
-
"FieldTypeVar",
|
|
36
36
|
)
|
|
37
37
|
|
|
38
38
|
import copy
|
|
39
39
|
import importlib
|
|
40
40
|
import io
|
|
41
41
|
import math
|
|
42
|
+
import numbers
|
|
42
43
|
import os
|
|
43
44
|
import re
|
|
44
45
|
import shutil
|
|
@@ -134,9 +135,15 @@ def _autocast(x, dtype):
|
|
|
134
135
|
If appropriate, the returned value is ``x`` cast to the given type
|
|
135
136
|
``dtype``. If the cast cannot be performed the original value of
|
|
136
137
|
``x`` is returned.
|
|
138
|
+
|
|
139
|
+
Notes
|
|
140
|
+
-----
|
|
141
|
+
Will convert numpy scalar types to the standard Python equivalents.
|
|
137
142
|
"""
|
|
138
|
-
if dtype is float and isinstance(x,
|
|
143
|
+
if dtype is float and isinstance(x, numbers.Real):
|
|
139
144
|
return float(x)
|
|
145
|
+
if dtype is int and isinstance(x, numbers.Integral):
|
|
146
|
+
return int(x)
|
|
140
147
|
return x
|
|
141
148
|
|
|
142
149
|
|
|
@@ -383,7 +390,6 @@ class Field(Generic[FieldTypeVar]):
|
|
|
383
390
|
>>> class Example(Config):
|
|
384
391
|
... myInt = Field("An integer field.", int, default=0)
|
|
385
392
|
... name = Field[str](doc="A string Field")
|
|
386
|
-
...
|
|
387
393
|
>>> print(config.myInt)
|
|
388
394
|
0
|
|
389
395
|
>>> config.myInt = 5
|
|
@@ -694,6 +700,14 @@ class Field(Generic[FieldTypeVar]):
|
|
|
694
700
|
"""
|
|
695
701
|
return self.__get__(instance)
|
|
696
702
|
|
|
703
|
+
def _copy_storage(self, old: Config, new: Config) -> Any:
|
|
704
|
+
"""Copy the storage for this field in the given field into an object
|
|
705
|
+
suitable for storage in a new copy of that config.
|
|
706
|
+
|
|
707
|
+
Any frozen storage should be unfrozen.
|
|
708
|
+
"""
|
|
709
|
+
return copy.deepcopy(old._storage[self.name])
|
|
710
|
+
|
|
697
711
|
@overload
|
|
698
712
|
def __get__(
|
|
699
713
|
self, instance: None, owner: Any = None, at: Any = None, label: str = "default"
|
|
@@ -729,7 +743,7 @@ class Field(Generic[FieldTypeVar]):
|
|
|
729
743
|
else:
|
|
730
744
|
raise AttributeError(
|
|
731
745
|
f"Config {instance} is missing _storage attribute, likely incorrectly initialized"
|
|
732
|
-
)
|
|
746
|
+
) from None
|
|
733
747
|
|
|
734
748
|
def __set__(
|
|
735
749
|
self, instance: Config, value: FieldTypeVar | None, at: Any = None, label: str = "assignment"
|
|
@@ -784,7 +798,7 @@ class Field(Generic[FieldTypeVar]):
|
|
|
784
798
|
try:
|
|
785
799
|
self._validateValue(value)
|
|
786
800
|
except BaseException as e:
|
|
787
|
-
raise FieldValidationError(self, instance, str(e))
|
|
801
|
+
raise FieldValidationError(self, instance, str(e)) from e
|
|
788
802
|
|
|
789
803
|
instance._storage[self.name] = value
|
|
790
804
|
if at is None:
|
|
@@ -936,23 +950,25 @@ class Config(metaclass=ConfigMeta): # type: ignore
|
|
|
936
950
|
>>> from lsst.pex.config import Config, Field, ListField
|
|
937
951
|
>>> class DemoConfig(Config):
|
|
938
952
|
... intField = Field(doc="An integer field", dtype=int, default=42)
|
|
939
|
-
... listField = ListField(
|
|
940
|
-
...
|
|
941
|
-
...
|
|
953
|
+
... listField = ListField(
|
|
954
|
+
... doc="List of favorite beverages.",
|
|
955
|
+
... dtype=str,
|
|
956
|
+
... default=["coffee", "green tea", "water"],
|
|
957
|
+
... )
|
|
942
958
|
>>> config = DemoConfig()
|
|
943
959
|
|
|
944
960
|
Configs support many `dict`-like APIs:
|
|
945
961
|
|
|
946
962
|
>>> config.keys()
|
|
947
963
|
['intField', 'listField']
|
|
948
|
-
>>>
|
|
964
|
+
>>> "intField" in config
|
|
949
965
|
True
|
|
950
966
|
|
|
951
967
|
Individual fields can be accessed as attributes of the configuration:
|
|
952
968
|
|
|
953
969
|
>>> config.intField
|
|
954
970
|
42
|
|
955
|
-
>>> config.listField.append(
|
|
971
|
+
>>> config.listField.append("earl grey tea")
|
|
956
972
|
>>> print(config.listField)
|
|
957
973
|
['coffee', 'green tea', 'water', 'earl grey tea']
|
|
958
974
|
"""
|
|
@@ -1047,6 +1063,30 @@ class Config(metaclass=ConfigMeta): # type: ignore
|
|
|
1047
1063
|
instance.update(__at=at, **kw)
|
|
1048
1064
|
return instance
|
|
1049
1065
|
|
|
1066
|
+
def copy(self) -> Config:
|
|
1067
|
+
"""Return a deep copy of this config.
|
|
1068
|
+
|
|
1069
|
+
Notes
|
|
1070
|
+
-----
|
|
1071
|
+
The returned config object is not frozen, even if the original was.
|
|
1072
|
+
If a nested config object is copied, it retains the name from its
|
|
1073
|
+
original hierarchy.
|
|
1074
|
+
|
|
1075
|
+
Nested objects are only shared between the new and old configs if they
|
|
1076
|
+
are not possible to modify via the config's interfaces (e.g. entries
|
|
1077
|
+
in the the history list are not copied, but the lists themselves are,
|
|
1078
|
+
so modifications to one copy do not modify the other).
|
|
1079
|
+
"""
|
|
1080
|
+
instance = object.__new__(type(self))
|
|
1081
|
+
instance._frozen = False
|
|
1082
|
+
instance._name = self._name
|
|
1083
|
+
instance._history = {k: list(v) for k, v in self._history.items()}
|
|
1084
|
+
instance._imports = set(self._imports)
|
|
1085
|
+
# Important to set up storage last, since fields sometimes store
|
|
1086
|
+
# proxy objects that reference their parent (especially for history).
|
|
1087
|
+
instance._storage = {k: self._fields[k]._copy_storage(self, instance) for k in self._storage}
|
|
1088
|
+
return instance
|
|
1089
|
+
|
|
1050
1090
|
def __reduce__(self):
|
|
1051
1091
|
"""Reduction for pickling (function with arguments to reproduce).
|
|
1052
1092
|
|
|
@@ -1095,30 +1135,27 @@ class Config(metaclass=ConfigMeta): # type: ignore
|
|
|
1095
1135
|
|
|
1096
1136
|
>>> from lsst.pex.config import Config, Field
|
|
1097
1137
|
>>> class DemoConfig(Config):
|
|
1098
|
-
... fieldA = Field(doc=
|
|
1099
|
-
... fieldB = Field(doc=
|
|
1100
|
-
... fieldC = Field(doc=
|
|
1101
|
-
...
|
|
1138
|
+
... fieldA = Field(doc="Field A", dtype=int, default=42)
|
|
1139
|
+
... fieldB = Field(doc="Field B", dtype=bool, default=True)
|
|
1140
|
+
... fieldC = Field(doc="Field C", dtype=str, default="Hello world")
|
|
1102
1141
|
>>> config = DemoConfig()
|
|
1103
1142
|
|
|
1104
1143
|
These are the default values of each field:
|
|
1105
1144
|
|
|
1106
1145
|
>>> for name, value in config.iteritems():
|
|
1107
1146
|
... print(f"{name}: {value}")
|
|
1108
|
-
...
|
|
1109
1147
|
fieldA: 42
|
|
1110
1148
|
fieldB: True
|
|
1111
1149
|
fieldC: 'Hello world'
|
|
1112
1150
|
|
|
1113
1151
|
Using this method to update ``fieldA`` and ``fieldC``:
|
|
1114
1152
|
|
|
1115
|
-
>>> config.update(fieldA=13, fieldC=
|
|
1153
|
+
>>> config.update(fieldA=13, fieldC="Updated!")
|
|
1116
1154
|
|
|
1117
1155
|
Now the values of each field are:
|
|
1118
1156
|
|
|
1119
1157
|
>>> for name, value in config.iteritems():
|
|
1120
1158
|
... print(f"{name}: {value}")
|
|
1121
|
-
...
|
|
1122
1159
|
fieldA: 13
|
|
1123
1160
|
fieldB: True
|
|
1124
1161
|
fieldC: 'Updated!'
|
|
@@ -1130,8 +1167,9 @@ class Config(metaclass=ConfigMeta): # type: ignore
|
|
|
1130
1167
|
try:
|
|
1131
1168
|
field = self._fields[name]
|
|
1132
1169
|
field.__set__(self, value, at=at, label=label)
|
|
1133
|
-
except KeyError:
|
|
1134
|
-
|
|
1170
|
+
except KeyError as e:
|
|
1171
|
+
e.add_note(f"No field of name {name} exists in config type {_typeStr(self)}")
|
|
1172
|
+
raise
|
|
1135
1173
|
|
|
1136
1174
|
def load(self, filename, root="config"):
|
|
1137
1175
|
"""Modify this config in place by executing the Python code in a
|
|
@@ -1408,7 +1446,7 @@ class Config(metaclass=ConfigMeta): # type: ignore
|
|
|
1408
1446
|
class.
|
|
1409
1447
|
"""
|
|
1410
1448
|
self._imports.add(self.__module__)
|
|
1411
|
-
for
|
|
1449
|
+
for field in self._fields.values():
|
|
1412
1450
|
field._collectImports(self, self._imports)
|
|
1413
1451
|
|
|
1414
1452
|
def toDict(self):
|
|
@@ -30,7 +30,6 @@ __all__ = ["ConfigChoiceField"]
|
|
|
30
30
|
|
|
31
31
|
import collections.abc
|
|
32
32
|
import copy
|
|
33
|
-
import weakref
|
|
34
33
|
from typing import Any, ForwardRef, overload
|
|
35
34
|
|
|
36
35
|
from .callStack import getCallStack, getStackFrame
|
|
@@ -63,35 +62,34 @@ class SelectionSet(collections.abc.MutableSet):
|
|
|
63
62
|
history.
|
|
64
63
|
"""
|
|
65
64
|
|
|
66
|
-
def __init__(
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
dict_: ConfigInstanceDict,
|
|
68
|
+
value: Any,
|
|
69
|
+
at=None,
|
|
70
|
+
label: str = "assignment",
|
|
71
|
+
setHistory: bool = True,
|
|
72
|
+
):
|
|
67
73
|
if at is None:
|
|
68
74
|
at = getCallStack()
|
|
69
75
|
self._dict = dict_
|
|
70
76
|
self._field = self._dict._field
|
|
71
|
-
self.
|
|
72
|
-
self.__history = self._config._history.setdefault(self._field.name, [])
|
|
77
|
+
self._history = self._dict._config._history.setdefault(self._field.name, [])
|
|
73
78
|
if value is not None:
|
|
74
79
|
try:
|
|
75
80
|
for v in value:
|
|
76
81
|
if v not in self._dict:
|
|
77
82
|
# invoke __getitem__ to ensure it's present
|
|
78
83
|
self._dict.__getitem__(v, at=at)
|
|
79
|
-
except TypeError:
|
|
84
|
+
except TypeError as e:
|
|
80
85
|
msg = f"Value {value} is of incorrect type {_typeStr(value)}. Sequence type expected"
|
|
81
|
-
raise FieldValidationError(self._field, self._config, msg)
|
|
86
|
+
raise FieldValidationError(self._field, self._dict._config, msg) from e
|
|
82
87
|
self._set = set(value)
|
|
83
88
|
else:
|
|
84
89
|
self._set = set()
|
|
85
90
|
|
|
86
91
|
if setHistory:
|
|
87
|
-
self.
|
|
88
|
-
|
|
89
|
-
@property
|
|
90
|
-
def _config(self) -> Config:
|
|
91
|
-
# Config Fields should never outlive their config class instance
|
|
92
|
-
# assert that as such here
|
|
93
|
-
assert self._config_() is not None
|
|
94
|
-
return self._config_()
|
|
92
|
+
self._history.append((f"Set selection to {self}", at, label))
|
|
95
93
|
|
|
96
94
|
def add(self, value, at=None):
|
|
97
95
|
"""Add a value to the selected set.
|
|
@@ -104,7 +102,7 @@ class SelectionSet(collections.abc.MutableSet):
|
|
|
104
102
|
optional
|
|
105
103
|
Stack frames for history recording.
|
|
106
104
|
"""
|
|
107
|
-
if self._config._frozen:
|
|
105
|
+
if self._dict._config._frozen:
|
|
108
106
|
raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config")
|
|
109
107
|
|
|
110
108
|
if at is None:
|
|
@@ -114,7 +112,7 @@ class SelectionSet(collections.abc.MutableSet):
|
|
|
114
112
|
# invoke __getitem__ to make sure it's present
|
|
115
113
|
self._dict.__getitem__(value, at=at)
|
|
116
114
|
|
|
117
|
-
self.
|
|
115
|
+
self._history.append((f"added {value} to selection", at, "selection"))
|
|
118
116
|
self._set.add(value)
|
|
119
117
|
|
|
120
118
|
def discard(self, value, at=None):
|
|
@@ -128,8 +126,8 @@ class SelectionSet(collections.abc.MutableSet):
|
|
|
128
126
|
optional
|
|
129
127
|
Stack frames for history recording.
|
|
130
128
|
"""
|
|
131
|
-
if self._config._frozen:
|
|
132
|
-
raise FieldValidationError(self._field, self._config, "Cannot modify a frozen Config")
|
|
129
|
+
if self._dict._config._frozen:
|
|
130
|
+
raise FieldValidationError(self._field, self._dict._config, "Cannot modify a frozen Config")
|
|
133
131
|
|
|
134
132
|
if value not in self._dict:
|
|
135
133
|
return
|
|
@@ -137,7 +135,7 @@ class SelectionSet(collections.abc.MutableSet):
|
|
|
137
135
|
if at is None:
|
|
138
136
|
at = getCallStack()
|
|
139
137
|
|
|
140
|
-
self.
|
|
138
|
+
self._history.append((f"removed {value} from selection", at, "selection"))
|
|
141
139
|
self._set.discard(value)
|
|
142
140
|
|
|
143
141
|
def __len__(self):
|
|
@@ -177,9 +175,9 @@ class ConfigInstanceDict(collections.abc.Mapping[str, Config]):
|
|
|
177
175
|
(that is, ``typemap[name]``).
|
|
178
176
|
"""
|
|
179
177
|
|
|
180
|
-
def __init__(self, config, field):
|
|
178
|
+
def __init__(self, config: Config, field: ConfigChoiceField):
|
|
181
179
|
collections.abc.Mapping.__init__(self)
|
|
182
|
-
self._dict = {}
|
|
180
|
+
self._dict: dict[str, Config] = {}
|
|
183
181
|
self._selection = None
|
|
184
182
|
self._config = config
|
|
185
183
|
self._field = field
|
|
@@ -187,6 +185,18 @@ class ConfigInstanceDict(collections.abc.Mapping[str, Config]):
|
|
|
187
185
|
self.__doc__ = field.doc
|
|
188
186
|
self._typemap = None
|
|
189
187
|
|
|
188
|
+
def _copy(self, config: Config) -> ConfigInstanceDict:
|
|
189
|
+
result = type(self)(config, self._field)
|
|
190
|
+
result._dict = {k: v.copy() for k, v in self._dict.items()}
|
|
191
|
+
result._history.extend(self._history)
|
|
192
|
+
result._typemap = self._typemap
|
|
193
|
+
if self._selection is not None:
|
|
194
|
+
if self._field.multi:
|
|
195
|
+
result._selection = SelectionSet(result._dict, self._selection._set)
|
|
196
|
+
else:
|
|
197
|
+
result._selection = self._selection
|
|
198
|
+
return result
|
|
199
|
+
|
|
190
200
|
@property
|
|
191
201
|
def types(self):
|
|
192
202
|
return self._typemap if self._typemap is not None else self._field.typemap
|
|
@@ -293,10 +303,10 @@ class ConfigInstanceDict(collections.abc.Mapping[str, Config]):
|
|
|
293
303
|
except KeyError:
|
|
294
304
|
try:
|
|
295
305
|
dtype = self.types[k]
|
|
296
|
-
except Exception:
|
|
306
|
+
except Exception as e:
|
|
297
307
|
raise FieldValidationError(
|
|
298
308
|
self._field, self._config, f"Unknown key {k!r} in Registry/ConfigChoiceField"
|
|
299
|
-
)
|
|
309
|
+
) from e
|
|
300
310
|
name = _joinNamePath(self._config._name, self._field.name, k)
|
|
301
311
|
if at is None:
|
|
302
312
|
at = getCallStack()
|
|
@@ -310,8 +320,8 @@ class ConfigInstanceDict(collections.abc.Mapping[str, Config]):
|
|
|
310
320
|
|
|
311
321
|
try:
|
|
312
322
|
dtype = self.types[k]
|
|
313
|
-
except Exception:
|
|
314
|
-
raise FieldValidationError(self._field, self._config, f"Unknown key {k!r}")
|
|
323
|
+
except Exception as e:
|
|
324
|
+
raise FieldValidationError(self._field, self._config, f"Unknown key {k!r}") from e
|
|
315
325
|
|
|
316
326
|
if value != dtype and type(value) is not dtype:
|
|
317
327
|
msg = (
|
|
@@ -443,7 +453,6 @@ class ConfigChoiceField(Field[ConfigInstanceDict]):
|
|
|
443
453
|
>>> from lsst.pex.config import Config, ConfigChoiceField, Field
|
|
444
454
|
>>> class AaaConfig(Config):
|
|
445
455
|
... somefield = Field("doc", int)
|
|
446
|
-
...
|
|
447
456
|
|
|
448
457
|
The ``MyConfig`` config has a ``ConfigChoiceField`` field called ``choice``
|
|
449
458
|
that maps the ``AaaConfig`` type to the ``"AAA"`` key:
|
|
@@ -451,7 +460,6 @@ class ConfigChoiceField(Field[ConfigInstanceDict]):
|
|
|
451
460
|
>>> TYPEMAP = {"AAA", AaaConfig}
|
|
452
461
|
>>> class MyConfig(Config):
|
|
453
462
|
... choice = ConfigChoiceField("doc for choice", TYPEMAP)
|
|
454
|
-
...
|
|
455
463
|
|
|
456
464
|
Creating an instance of ``MyConfig``:
|
|
457
465
|
|
|
@@ -460,7 +468,7 @@ class ConfigChoiceField(Field[ConfigInstanceDict]):
|
|
|
460
468
|
Setting value of the field ``somefield`` on the "AAA" key of the ``choice``
|
|
461
469
|
field:
|
|
462
470
|
|
|
463
|
-
>>> instance.choice[
|
|
471
|
+
>>> instance.choice["AAA"].somefield = 5
|
|
464
472
|
|
|
465
473
|
**Selecting the active configuration**
|
|
466
474
|
|
|
@@ -546,6 +554,13 @@ class ConfigChoiceField(Field[ConfigInstanceDict]):
|
|
|
546
554
|
else:
|
|
547
555
|
instanceDict._setSelection(value, at=at, label=label)
|
|
548
556
|
|
|
557
|
+
def _copy_storage(self, old: Config, new: Config) -> Any:
|
|
558
|
+
instance_dict: ConfigInstanceDict | None = old._storage.get(self.name)
|
|
559
|
+
if instance_dict is not None:
|
|
560
|
+
return instance_dict._copy(new)
|
|
561
|
+
else:
|
|
562
|
+
return None
|
|
563
|
+
|
|
549
564
|
def rename(self, instance):
|
|
550
565
|
instanceDict = self.__get__(instance)
|
|
551
566
|
fullname = _joinNamePath(instance._name, self.name)
|
|
@@ -24,10 +24,13 @@
|
|
|
24
24
|
#
|
|
25
25
|
# You should have received a copy of the GNU General Public License
|
|
26
26
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
27
|
+
from __future__ import annotations
|
|
27
28
|
|
|
28
29
|
__all__ = ["ConfigDictField"]
|
|
29
30
|
|
|
30
|
-
from .
|
|
31
|
+
from collections.abc import Mapping
|
|
32
|
+
|
|
33
|
+
from .callStack import StackFrame, getCallStack, getStackFrame
|
|
31
34
|
from .comparison import compareConfigs, compareScalars, getComparisonName
|
|
32
35
|
from .config import Config, FieldValidationError, _autocast, _joinNamePath, _typeStr
|
|
33
36
|
from .dictField import Dict, DictField
|
|
@@ -51,11 +54,33 @@ class ConfigDict(Dict[str, Config]):
|
|
|
51
54
|
Stack frame for history recording. Will be calculated if `None`.
|
|
52
55
|
label : `str`, optional
|
|
53
56
|
Label to use for history recording.
|
|
57
|
+
setHistory : `bool`, optional
|
|
58
|
+
Whether to append to the history record.
|
|
54
59
|
"""
|
|
55
60
|
|
|
56
|
-
def __init__(
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
config: Config,
|
|
64
|
+
field: ConfigDictField,
|
|
65
|
+
value: Mapping[str, Config] | None,
|
|
66
|
+
*,
|
|
67
|
+
at: list[StackFrame] | None,
|
|
68
|
+
label: str,
|
|
69
|
+
setHistory: bool = True,
|
|
70
|
+
):
|
|
71
|
+
Dict.__init__(self, config, field, value, at=at, label=label, setHistory=False)
|
|
72
|
+
if setHistory:
|
|
73
|
+
self.history.append(("Dict initialized", at, label))
|
|
74
|
+
|
|
75
|
+
def _copy(self, config: Config) -> Dict:
|
|
76
|
+
return type(self)(
|
|
77
|
+
config,
|
|
78
|
+
self._field,
|
|
79
|
+
{k: v._copy() for k, v in self._dict.items()},
|
|
80
|
+
at=None,
|
|
81
|
+
label="copy",
|
|
82
|
+
setHistory=False,
|
|
83
|
+
)
|
|
59
84
|
|
|
60
85
|
def __setitem__(self, k, x, at=None, label="setitem", setHistory=True):
|
|
61
86
|
if self._config._frozen:
|
|
@@ -77,6 +102,11 @@ class ConfigDict(Dict[str, Config]):
|
|
|
77
102
|
)
|
|
78
103
|
raise FieldValidationError(self._field, self._config, msg)
|
|
79
104
|
|
|
105
|
+
# validate key using keycheck
|
|
106
|
+
if self._field.keyCheck is not None and not self._field.keyCheck(k):
|
|
107
|
+
msg = f"Key {k!r} is not a valid key"
|
|
108
|
+
raise FieldValidationError(self._field, self._config, msg)
|
|
109
|
+
|
|
80
110
|
if at is None:
|
|
81
111
|
at = getCallStack()
|
|
82
112
|
name = _joinNamePath(self._config._name, self._field.name, k)
|
|
@@ -127,6 +157,8 @@ class ConfigDictField(DictField):
|
|
|
127
157
|
Default is `True`.
|
|
128
158
|
dictCheck : `~collections.abc.Callable` or `None`, optional
|
|
129
159
|
Callable to check a dict.
|
|
160
|
+
keyCheck : `~collections.abc.Callable` or `None`, optional
|
|
161
|
+
Callable to check a key.
|
|
130
162
|
itemCheck : `~collections.abc.Callable` or `None`, optional
|
|
131
163
|
Callable to check an item.
|
|
132
164
|
deprecated : None or `str`, optional
|
|
@@ -140,7 +172,8 @@ class ConfigDictField(DictField):
|
|
|
140
172
|
|
|
141
173
|
- ``keytype`` or ``itemtype`` arguments are not supported types
|
|
142
174
|
(members of `ConfigDictField.supportedTypes`.
|
|
143
|
-
- ``dictCheck`` or ``itemCheck`` is not a callable
|
|
175
|
+
- ``dictCheck``, ``keyCheck`` or ``itemCheck`` is not a callable
|
|
176
|
+
function.
|
|
144
177
|
|
|
145
178
|
See Also
|
|
146
179
|
--------
|
|
@@ -172,6 +205,7 @@ class ConfigDictField(DictField):
|
|
|
172
205
|
default=None,
|
|
173
206
|
optional=False,
|
|
174
207
|
dictCheck=None,
|
|
208
|
+
keyCheck=None,
|
|
175
209
|
itemCheck=None,
|
|
176
210
|
deprecated=None,
|
|
177
211
|
):
|
|
@@ -189,14 +223,18 @@ class ConfigDictField(DictField):
|
|
|
189
223
|
raise ValueError(f"'keytype' {_typeStr(keytype)} is not a supported type")
|
|
190
224
|
elif not issubclass(itemtype, Config):
|
|
191
225
|
raise ValueError(f"'itemtype' {_typeStr(itemtype)} is not a supported type")
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
226
|
+
|
|
227
|
+
check_errors = []
|
|
228
|
+
for name, check in (("dictCheck", dictCheck), ("keyCheck", keyCheck), ("itemCheck", itemCheck)):
|
|
229
|
+
if check is not None and not callable(check):
|
|
230
|
+
check_errors.append(name)
|
|
231
|
+
if check_errors:
|
|
232
|
+
raise ValueError(f"{', '.join(check_errors)} must be callable")
|
|
196
233
|
|
|
197
234
|
self.keytype = keytype
|
|
198
235
|
self.itemtype = itemtype
|
|
199
236
|
self.dictCheck = dictCheck
|
|
237
|
+
self.keyCheck = keyCheck
|
|
200
238
|
self.itemCheck = itemCheck
|
|
201
239
|
|
|
202
240
|
def rename(self, instance):
|
|
@@ -207,6 +245,23 @@ class ConfigDictField(DictField):
|
|
|
207
245
|
configDict[k]._rename(fullname)
|
|
208
246
|
|
|
209
247
|
def validate(self, instance):
|
|
248
|
+
"""Validate the field.
|
|
249
|
+
|
|
250
|
+
Parameters
|
|
251
|
+
----------
|
|
252
|
+
instance : `lsst.pex.config.Config`
|
|
253
|
+
The config instance that contains this field.
|
|
254
|
+
|
|
255
|
+
Raises
|
|
256
|
+
------
|
|
257
|
+
lsst.pex.config.FieldValidationError
|
|
258
|
+
Raised if validation fails for this field.
|
|
259
|
+
|
|
260
|
+
Notes
|
|
261
|
+
-----
|
|
262
|
+
Individual key checks (``keyCheck``) are applied when each key is added
|
|
263
|
+
and are not re-checked by this method.
|
|
264
|
+
"""
|
|
210
265
|
value = self.__get__(instance)
|
|
211
266
|
if value is not None:
|
|
212
267
|
for k in value:
|
|
@@ -289,7 +344,7 @@ class ConfigDictField(DictField):
|
|
|
289
344
|
name = getComparisonName(
|
|
290
345
|
_joinNamePath(instance1._name, self.name), _joinNamePath(instance2._name, self.name)
|
|
291
346
|
)
|
|
292
|
-
if not compareScalars(f"
|
|
347
|
+
if not compareScalars(f"{name} (keys)", set(d1.keys()), set(d2.keys()), output=output):
|
|
293
348
|
return False
|
|
294
349
|
equal = True
|
|
295
350
|
for k, v1 in d1.items():
|
lsst/pex/config/configField.py
CHANGED
|
@@ -243,6 +243,13 @@ class ConfigField(Field[FieldTypeVar]):
|
|
|
243
243
|
value = self.__get__(instance)
|
|
244
244
|
return value.toDict()
|
|
245
245
|
|
|
246
|
+
def _copy_storage(self, old: Config, new: Config) -> Any:
|
|
247
|
+
value: Config | None = old._storage.get(self.name)
|
|
248
|
+
if value is not None:
|
|
249
|
+
return value.copy()
|
|
250
|
+
else:
|
|
251
|
+
return None
|
|
252
|
+
|
|
246
253
|
def validate(self, instance):
|
|
247
254
|
"""Validate the field (for internal use only).
|
|
248
255
|
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
23
|
-
__all__ = ["
|
|
23
|
+
__all__ = ["ActionTypeVar", "ConfigurableAction"]
|
|
24
24
|
|
|
25
25
|
from typing import Any, TypeVar
|
|
26
26
|
|
|
@@ -62,3 +62,8 @@ class ConfigurableAction(Config):
|
|
|
62
62
|
|
|
63
63
|
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
|
64
64
|
raise NotImplementedError("This method should be overloaded in subclasses")
|
|
65
|
+
|
|
66
|
+
def copy(self) -> ConfigurableAction:
|
|
67
|
+
result = super().copy()
|
|
68
|
+
result.identity = self.identity
|
|
69
|
+
return result
|