Skip to content

Commit 60b93e7

Browse files
authored
Merge branch 'develop' into pno/review_max_contributions_per_user
2 parents 3fb7b96 + 6e0c52e commit 60b93e7

File tree

2 files changed

+49
-5
lines changed

2 files changed

+49
-5
lines changed

libs/labelbox/src/labelbox/schema/tool_building/classification.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,15 @@ class UIMode(Enum):
7676
None # How this classification should be answered (e.g. hotkeys / autocomplete, etc)
7777
)
7878
attributes: Optional[FeatureSchemaAttributes] = None
79+
is_likert_scale: bool = False
7980

8081
def __post_init__(self):
8182
if self.name is None:
8283
msg = (
83-
"When creating the Classification feature, please use name” "
84+
'When creating the Classification feature, please use "name" '
8485
"for the classification schema name, which will be used when "
8586
"creating annotation payload for Model-Assisted Labeling "
86-
"Import and Label Import. instructions is no longer "
87+
'Import and Label Import. "instructions" is no longer '
8788
"supported to specify classification schema name."
8889
)
8990
if self.instructions is not None:
@@ -119,6 +120,7 @@ def from_dict(cls, dictionary: Dict[str, Any]) -> "Classification":
119120
]
120121
if dictionary.get("attributes")
121122
else None,
123+
is_likert_scale=dictionary.get("isLikertScale", False),
122124
)
123125

124126
def asdict(self, is_subclass: bool = False) -> Dict[str, Any]:
@@ -138,6 +140,9 @@ def asdict(self, is_subclass: bool = False) -> Dict[str, Any]:
138140
if self.attributes is not None
139141
else None,
140142
}
143+
if self.class_type == self.Type.RADIO and self.is_likert_scale:
144+
# is_likert_scale is only applicable to RADIO classifications
145+
classification["isLikertScale"] = self.is_likert_scale
141146
if (
142147
self.class_type == self.Type.RADIO
143148
or self.class_type == self.Type.CHECKLIST
@@ -159,6 +164,9 @@ def add_option(self, option: "Option") -> None:
159164
f"Duplicate option '{option.value}' "
160165
f"for classification '{self.name}'."
161166
)
167+
# Auto-assign position if not set
168+
if option.position is None:
169+
option.position = len(self.options)
162170
self.options.append(option)
163171

164172

@@ -178,6 +186,7 @@ class Option:
178186
schema_id: (str)
179187
feature_schema_id: (str)
180188
options: (list)
189+
position: (int) - Position of the option, auto-assigned starting from 0
181190
"""
182191

183192
value: Union[str, int]
@@ -187,6 +196,7 @@ class Option:
187196
options: Union[
188197
List["Classification"], List["PromptResponseClassification"]
189198
] = field(default_factory=list)
199+
position: Optional[int] = None
190200

191201
def __post_init__(self):
192202
if self.label is None:
@@ -203,16 +213,20 @@ def from_dict(cls, dictionary: Dict[str, Any]) -> "Option":
203213
Classification.from_dict(o)
204214
for o in dictionary.get("options", [])
205215
],
216+
position=dictionary.get("position", None),
206217
)
207218

208219
def asdict(self) -> Dict[str, Any]:
209-
return {
220+
result = {
210221
"schemaNodeId": self.schema_id,
211222
"featureSchemaId": self.feature_schema_id,
212223
"label": self.label,
213224
"value": self.value,
214225
"options": [o.asdict(is_subclass=True) for o in self.options],
215226
}
227+
if self.position is not None:
228+
result["position"] = self.position
229+
return result
216230

217231
def add_option(
218232
self, option: Union["Classification", "PromptResponseClassification"]
@@ -268,10 +282,10 @@ class PromptResponseClassification:
268282
def __post_init__(self):
269283
if self.name is None:
270284
msg = (
271-
"When creating the Classification feature, please use name” "
285+
'When creating the Classification feature, please use "name" '
272286
"for the classification schema name, which will be used when "
273287
"creating annotation payload for Model-Assisted Labeling "
274-
"Import and Label Import. instructions is no longer "
288+
'Import and Label Import. "instructions" is no longer '
275289
"supported to specify classification schema name."
276290
)
277291
if self.instructions is not None:

libs/labelbox/tests/unit/test_unit_ontology.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,3 +294,33 @@ def test_classification_using_instructions_instead_of_name_shows_warning():
294294
def test_classification_without_name_raises_error():
295295
with pytest.raises(ValueError):
296296
Classification(class_type=Classification.Type.TEXT)
297+
298+
299+
@pytest.mark.parametrize(
300+
"class_type, is_likert_scale, should_include",
301+
[
302+
(Classification.Type.RADIO, True, True),
303+
(Classification.Type.RADIO, False, False),
304+
(Classification.Type.CHECKLIST, True, False),
305+
(Classification.Type.TEXT, True, False),
306+
],
307+
)
308+
def test_is_likert_scale_serialization(
309+
class_type, is_likert_scale, should_include
310+
):
311+
c = Classification(
312+
class_type=class_type, name="test", is_likert_scale=is_likert_scale
313+
)
314+
if class_type in Classification._REQUIRES_OPTIONS:
315+
c.add_option(Option(value="option1"))
316+
result = c.asdict()
317+
assert ("isLikertScale" in result) == should_include
318+
319+
320+
def test_option_position_auto_assignment():
321+
c = Classification(class_type=Classification.Type.RADIO, name="test")
322+
o1, o2 = Option(value="first"), Option(value="second")
323+
c.add_option(o1)
324+
c.add_option(o2)
325+
assert o1.position == 0 and o2.position == 1
326+
assert c.asdict()["options"][0]["position"] == 0

0 commit comments

Comments
 (0)