Skip to content

Commit 3e1d496

Browse files
authored
Merge pull request #2878 from alicevision/dev/attributeValues
[core] Add support for keyable attributes
2 parents 64f1c65 + 9149d0a commit 3e1d496

File tree

8 files changed

+641
-35
lines changed

8 files changed

+641
-35
lines changed

meshroom/core/attribute.py

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from string import Template
1010
from meshroom.common import BaseObject, Property, Variant, Signal, ListModel, DictModel, Slot
1111
from meshroom.core import desc, hashValue
12-
12+
from meshroom.core.keyValues import KeyValues
1313
from typing import TYPE_CHECKING
1414

1515
if TYPE_CHECKING:
@@ -77,6 +77,7 @@ def __init__(self, node, attributeDesc: desc.Attribute, isOutput: bool, root=Non
7777
self._invalidate = False if self._isOutput else attributeDesc.invalidate
7878
self._invalidationValue = "" # invalidation value for output attributes
7979
self._value = None
80+
self._keyValues = None # list of pairs (key, value) for keyable attribute
8081
self._initValue()
8182

8283
def _getFullName(self) -> str:
@@ -119,7 +120,12 @@ def _initValue(self):
119120
Initialize the attribute value.
120121
Called in the attribute factory for each attributes.
121122
"""
122-
if self._desc._valueType is not None:
123+
if self._desc.keyable:
124+
# Keyable attribute, initialize keyValues from attribute description
125+
self._keyValues = KeyValues(self._desc)
126+
# Send signal and updates if keyValues changed
127+
self._keyValues.pairsChanged.connect(self._onKeyValuesChanged)
128+
elif self._desc._valueType is not None:
123129
self._value = self._desc._valueType()
124130

125131
def _getEvalValue(self):
@@ -144,6 +150,8 @@ def _getValue(self):
144150
"""
145151
Return the value of the attribute or the linked attribute value.
146152
"""
153+
if self.keyable:
154+
raise RuntimeError(f"Cannot get value of {self._getFullName()}, the attribute is keyable.")
147155
if self.isLink:
148156
return self._getInputLink().value
149157
return self._value
@@ -157,6 +165,14 @@ def _setValue(self, value):
157165
if isinstance(value, Attribute) or Attribute.isLinkExpression(value):
158166
# if we set a link to another attribute
159167
self._value = value
168+
if self.keyable:
169+
self._keyValues.reset()
170+
elif self.keyable and isinstance(value, dict):
171+
# keyable attribute initialize from a dict
172+
self.keyValues.resetFromDict(value)
173+
elif self.keyable:
174+
# keyable attribute but value is not a dict
175+
raise RuntimeError(f"Cannot set value of {self._getFullName()}, the attribute is keyable.")
160176
elif callable(value):
161177
# evaluate the function
162178
self._value = value(self)
@@ -178,6 +194,16 @@ def _setValue(self, value):
178194
self.requestNodeUpdate()
179195
self.valueChanged.emit()
180196

197+
def _getKeyValues(self):
198+
"""
199+
Return the per-key values object of the attribute or of the linked attribute.
200+
"""
201+
if not self.keyable:
202+
raise RuntimeError(f"Cannot get keyValues of {self._getFullName()}, the attribute is not keyable.")
203+
if self.isLink:
204+
return self._getInputLink().keyValues
205+
return self._keyValues
206+
181207
def _applyExpr(self):
182208
"""
183209
For string parameters with an expression (when loaded from file),
@@ -214,7 +240,11 @@ def resetToDefaultValue(self):
214240
"""
215241
Reset the attribute to its default value.
216242
"""
217-
self._setValue(copy.copy(self.getDefaultValue()))
243+
if self.keyable:
244+
self._value = None
245+
self._keyValues.reset()
246+
else:
247+
self._setValue(copy.copy(self.getDefaultValue()))
218248

219249
def getDefaultValue(self):
220250
"""
@@ -227,6 +257,9 @@ def getDefaultValue(self):
227257
if not self.node.isCompatibilityNode:
228258
logging.warning(f"Failed to evaluate 'defaultValue' (node lambda) for attribute '{self.fullName}': {e}")
229259
return None
260+
# keyable attribute default value
261+
if self.keyable:
262+
return {}
230263
# Need to force a copy, for the case where the value is a list
231264
# (avoid reference to the desc value)
232265
return copy.copy(self._desc.value)
@@ -237,6 +270,8 @@ def getSerializedValue(self):
237270
"""
238271
if self.isLink:
239272
return self._getInputLink().asLinkExpr()
273+
if self.keyable:
274+
return self._keyValues.getSerializedValues()
240275
if self.isOutput and self._desc.isExpression:
241276
return self.getDefaultValue()
242277
return self.value
@@ -252,6 +287,9 @@ def getValueStr(self, withQuotes=True) -> str:
252287
If it is an empty list, it will returns a really empty string.
253288
If it is a list with one empty string element, it will returns 2 quotes.
254289
"""
290+
# Keyable attribute, for now return the list of pairs as a JSON sting
291+
if self.keyable:
292+
return self._keyValues.getJson()
255293
# ChoiceParam with multiple values should be combined
256294
if isinstance(self._desc, desc.ChoiceParam) and not self._desc.exclusive:
257295
# Ensure value is a list as expected
@@ -278,6 +316,12 @@ def upgradeValue(self, exportedValue):
278316
"""
279317
self._setValue(exportedValue)
280318

319+
def _isDefault(self):
320+
if self.keyable:
321+
return len(self._keyValues.pairs) == 0
322+
else:
323+
return self._getValue() == self.getDefaultValue()
324+
281325
def _isValid(self):
282326
"""
283327
Check attribute description validValue:
@@ -333,6 +377,8 @@ def uid(self) -> str:
333377
if self.isLink:
334378
linkRootAttribute = self._getInputLink(recursive=True)
335379
return linkRootAttribute.uid()
380+
if self.keyable:
381+
return self._keyValues.uid()
336382
if isinstance(self._value, (list, tuple, set,)):
337383
# non-exclusive choice param
338384
# hash of sorted values hashed
@@ -425,6 +471,17 @@ def _hasAnyOutputLinks(self) -> bool:
425471

426472
# Slots
427473

474+
@Slot()
475+
def _onKeyValuesChanged(self):
476+
"""
477+
For keyable attribute, when the list or pairs (key, value) is modified this method should be called.
478+
Emit Attribute.valueChanged and update node / graph like _setValue().
479+
"""
480+
if self.isInput:
481+
self.requestGraphUpdate()
482+
self.requestNodeUpdate()
483+
self.valueChanged.emit()
484+
428485
@Slot()
429486
def _onValueChanged(self):
430487
self.node._onAttributeChanged(self)
@@ -469,9 +526,13 @@ def matchText(self, text: str) -> bool:
469526
valueChanged = Signal()
470527
value = Property(Variant, _getValue, _setValue, notify=valueChanged)
471528
evalValue = Property(Variant, _getEvalValue, notify=valueChanged)
529+
# Whether the attribute can have a distinct value per key.
530+
keyable = Property(bool, lambda self: self._desc.keyable, constant=True)
531+
# The list of pairs (key, value) of the attribute.
532+
keyValues = Property(Variant, _getKeyValues, notify=valueChanged)
472533

473534
# Whether the attribute value is the default value.
474-
isDefault = Property(bool, lambda self: self.value == self.getDefaultValue(), notify=valueChanged)
535+
isDefault = Property(bool, _isDefault, notify=valueChanged)
475536
# Whether the attribute value is valid.
476537
isValid = Property(bool, _isValid, notify=valueChanged)
477538
# Whether the attribute value is displayable in 2d.

meshroom/core/desc/attribute.py

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@ class Attribute(BaseObject):
1010
"""
1111
"""
1212

13-
def __init__(self, name, label, description, value, advanced, semantic, group, enabled, invalidate=True,
14-
uidIgnoreValue=None, validValue=True, errorMessage="", visible=True, exposed=False):
13+
def __init__(self, name, label, description, value, advanced, semantic, group, enabled,
14+
keyable=False, keyType=None, invalidate=True, uidIgnoreValue=None,
15+
validValue=True, errorMessage="", visible=True, exposed=False):
1516
super(Attribute, self).__init__()
1617
self._name = name
1718
self._label = label
1819
self._description = description
1920
self._value = value
21+
self._keyable = keyable
22+
self._keyType = keyType
2023
self._group = group
2124
self._advanced = advanced
2225
self._enabled = enabled
@@ -46,6 +49,15 @@ def validateValue(self, value):
4649
"""
4750
raise NotImplementedError("Attribute.validateValue is an abstract function that should be "
4851
"implemented in the derived class.")
52+
53+
def validateKeyValues(self, keyValues):
54+
""" Return validated/conformed 'keyValues'.
55+
56+
Raises:
57+
ValueError: if a value does not have the proper type
58+
"""
59+
return isinstance(keyValues, dict) and \
60+
all(isinstance(k, str) and self.validateValue(v) for k,v in keyValues.items())
4961

5062
def checkValueTypes(self):
5163
""" Returns the attribute's name if the default value's type is invalid or if the range's type (when available)
@@ -65,7 +77,10 @@ def matchDescription(self, value, strict=True):
6577
strict: strict test for the match (for instance, regarding a group with some parameter changes)
6678
"""
6779
try:
68-
self.validateValue(value)
80+
if self._keyable:
81+
self.validateKeyValues(value)
82+
else:
83+
self.validateValue(value)
6984
except ValueError:
7085
return False
7186
return True
@@ -82,6 +97,14 @@ def matchDescription(self, value, strict=True):
8297
# The default value of the attribute's descriptor is None, so it is not an input value,
8398
# but an output value that is computed during the Node's process execution.
8499
isDynamicValue = Property(bool, lambda self: self._isDynamicValue, constant=True)
100+
# keyable:
101+
# Whether the attribute can have a distinct value per key.
102+
# By default, atribute value is not keyable.
103+
keyable = Property(bool, lambda self: self._keyable, constant=True)
104+
# keyType:
105+
# The type of key corresponding to the attribute value.
106+
# This property only makes sense for keyable attributes.
107+
keyType = Property(str, lambda self: self._keyType, constant=True)
85108
group = Property(str, lambda self: self._group, constant=True)
86109
advanced = Property(bool, lambda self: self._advanced, constant=True)
87110
enabled = Property(Variant, lambda self: self._enabled, constant=True)
@@ -265,11 +288,13 @@ def retrieveChildrenInvalidations(self):
265288
class Param(Attribute):
266289
"""
267290
"""
268-
def __init__(self, name, label, description, value, group, advanced, semantic, enabled, invalidate=True,
269-
uidIgnoreValue=None, validValue=True, errorMessage="", visible=True, exposed=False):
291+
def __init__(self, name, label, description, value, group, advanced, semantic, enabled,
292+
keyable=False, keyType=None, invalidate=True, uidIgnoreValue=None,
293+
validValue=True, errorMessage="", visible=True, exposed=False):
270294
super(Param, self).__init__(name=name, label=label, description=description, value=value,
271-
group=group, advanced=advanced, enabled=enabled, invalidate=invalidate,
272-
semantic=semantic, uidIgnoreValue=uidIgnoreValue, validValue=validValue,
295+
keyable=keyable, keyType=keyType, group=group, advanced=advanced,
296+
enabled=enabled, invalidate=invalidate, semantic=semantic,
297+
uidIgnoreValue=uidIgnoreValue, validValue=validValue,
273298
errorMessage=errorMessage, visible=visible, exposed=exposed)
274299

275300

@@ -302,11 +327,13 @@ def checkValueTypes(self):
302327
class BoolParam(Param):
303328
"""
304329
"""
305-
def __init__(self, name, label, description, value, group="allParams", advanced=False, enabled=True,
306-
invalidate=True, semantic="", visible=True, exposed=False):
330+
def __init__(self, name, label, description, value, keyable=False, keyType=None,
331+
group="allParams", advanced=False, enabled=True, invalidate=True,
332+
semantic="", visible=True, exposed=False):
307333
super(BoolParam, self).__init__(name=name, label=label, description=description, value=value,
308-
group=group, advanced=advanced, enabled=enabled, invalidate=invalidate,
309-
semantic=semantic, visible=visible, exposed=exposed)
334+
keyable=keyable, keyType=keyType, group=group, advanced=advanced,
335+
enabled=enabled, invalidate=invalidate, semantic=semantic,
336+
visible=visible, exposed=exposed)
310337
self._valueType = bool
311338

312339
def validateValue(self, value):
@@ -330,12 +357,14 @@ def checkValueTypes(self):
330357
class IntParam(Param):
331358
"""
332359
"""
333-
def __init__(self, name, label, description, value, range=None, group="allParams", advanced=False, enabled=True,
334-
invalidate=True, semantic="", validValue=True, errorMessage="", visible=True, exposed=False):
360+
def __init__(self, name, label, description, value, range=None, keyable=False, keyType=None,
361+
group="allParams", advanced=False, enabled=True, invalidate=True, semantic="",
362+
validValue=True, errorMessage="", visible=True, exposed=False):
335363
self._range = range
336364
super(IntParam, self).__init__(name=name, label=label, description=description, value=value,
337-
group=group, advanced=advanced, enabled=enabled, invalidate=invalidate,
338-
semantic=semantic, validValue=validValue, errorMessage=errorMessage,
365+
keyable=keyable, keyType=keyType, group=group, advanced=advanced,
366+
enabled=enabled, invalidate=invalidate, semantic=semantic,
367+
validValue=validValue, errorMessage=errorMessage,
339368
visible=visible, exposed=exposed)
340369
self._valueType = int
341370

@@ -360,12 +389,14 @@ def checkValueTypes(self):
360389
class FloatParam(Param):
361390
"""
362391
"""
363-
def __init__(self, name, label, description, value, range=None, group="allParams", advanced=False, enabled=True,
364-
invalidate=True, semantic="", validValue=True, errorMessage="", visible=True, exposed=False):
392+
def __init__(self, name, label, description, value, range=None, keyable=False, keyType=None,
393+
group="allParams", advanced=False, enabled=True, invalidate=True, semantic="",
394+
validValue=True, errorMessage="", visible=True, exposed=False):
365395
self._range = range
366396
super(FloatParam, self).__init__(name=name, label=label, description=description, value=value,
367-
group=group, advanced=advanced, enabled=enabled, invalidate=invalidate,
368-
semantic=semantic, validValue=validValue, errorMessage=errorMessage,
397+
keyable=keyable, keyType=keyType, group=group, advanced=advanced,
398+
enabled=enabled, invalidate=invalidate, semantic=semantic,
399+
validValue=validValue, errorMessage=errorMessage,
369400
visible=visible, exposed=exposed)
370401
self._valueType = float
371402

0 commit comments

Comments
 (0)