Skip to content

Commit 0802216

Browse files
authored
[PTDT-2967] Relationship read_only (#1950)
2 parents 3daa25b + 315ce1f commit 0802216

File tree

4 files changed

+164
-3
lines changed

4 files changed

+164
-3
lines changed

libs/labelbox/src/labelbox/data/annotation_types/relationship.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from typing import Union
2-
from pydantic import BaseModel
1+
from typing import Union, Optional
2+
from pydantic import BaseModel, model_validator
33
from enum import Enum
4+
import warnings
45
from labelbox.data.annotation_types.annotation import (
56
BaseAnnotation,
67
ObjectAnnotation,
@@ -16,6 +17,15 @@ class Type(Enum):
1617
source: Union[ObjectAnnotation, ClassificationAnnotation]
1718
target: ObjectAnnotation
1819
type: Type = Type.UNIDIRECTIONAL
20+
readonly: Optional[bool] = None
21+
22+
@model_validator(mode='after')
23+
def check_readonly(self):
24+
if self.readonly is True:
25+
warnings.warn(
26+
"Creating a relationship with readonly=True is in beta and its behavior may change in future releases.",
27+
)
28+
return self
1929

2030

2131
class RelationshipAnnotation(BaseAnnotation):

libs/labelbox/src/labelbox/data/serialization/ndjson/relationship.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from typing import Optional
12
from pydantic import BaseModel
23
from .base import NDAnnotation, DataRow
34
from ...annotation_types.data import GenericDataRowData
@@ -13,7 +14,7 @@ class _Relationship(BaseModel):
1314
source: str
1415
target: str
1516
type: str
16-
17+
readonly: Optional[bool] = None
1718

1819
class NDRelationship(NDAnnotation):
1920
relationship: _Relationship
@@ -30,6 +31,7 @@ def to_common(
3031
source=source,
3132
target=target,
3233
type=Relationship.Type(annotation.relationship.type),
34+
readonly=annotation.relationship.readonly,
3335
),
3436
extra={"uuid": annotation.uuid},
3537
feature_schema_id=annotation.schema_id,
@@ -50,5 +52,6 @@ def from_common(
5052
source=str(relationship.source._uuid),
5153
target=str(relationship.target._uuid),
5254
type=relationship.type.value,
55+
readonly=relationship.readonly,
5356
),
5457
)

libs/labelbox/tests/data/annotation_import/test_relationships.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,3 +337,72 @@ def test_classification_relationship_restrictions():
337337
data={"global_key": "test_key"}, annotations=[relationship]
338338
)
339339
list(NDJsonConverter.serialize([label]))
340+
341+
342+
def test_relationship_readonly_default_none():
343+
"""Test that relationship readonly field defaults to None when not specified."""
344+
source = ObjectAnnotation(
345+
name="e1",
346+
value=TextEntity(start=10, end=12),
347+
)
348+
target = ObjectAnnotation(
349+
name="e2",
350+
value=TextEntity(start=30, end=35),
351+
)
352+
353+
relationship = RelationshipAnnotation(
354+
name="rel",
355+
value=Relationship(
356+
source=source,
357+
target=target,
358+
type=Relationship.Type.UNIDIRECTIONAL,
359+
),
360+
)
361+
assert relationship.value.readonly is None
362+
363+
364+
def test_relationship_readonly_explicit_false():
365+
"""Test that relationship readonly field can be explicitly set to False."""
366+
source = ObjectAnnotation(
367+
name="e1",
368+
value=TextEntity(start=10, end=12),
369+
)
370+
target = ObjectAnnotation(
371+
name="e2",
372+
value=TextEntity(start=30, end=35),
373+
)
374+
375+
relationship = RelationshipAnnotation(
376+
name="rel",
377+
value=Relationship(
378+
source=source,
379+
target=target,
380+
type=Relationship.Type.UNIDIRECTIONAL,
381+
readonly=False,
382+
),
383+
)
384+
assert relationship.value.readonly is False
385+
386+
387+
def test_relationship_readonly_explicit_true():
388+
"""Test that setting relationship readonly=True triggers a warning."""
389+
source = ObjectAnnotation(
390+
name="e1",
391+
value=TextEntity(start=10, end=12),
392+
)
393+
target = ObjectAnnotation(
394+
name="e2",
395+
value=TextEntity(start=30, end=35),
396+
)
397+
398+
with pytest.warns(UserWarning, match="Creating a relationship with readonly=True is in beta.*"):
399+
relationship = RelationshipAnnotation(
400+
name="rel",
401+
value=Relationship(
402+
source=source,
403+
target=target,
404+
type=Relationship.Type.UNIDIRECTIONAL,
405+
readonly=True,
406+
),
407+
)
408+
assert relationship.value.readonly is True

libs/labelbox/tests/data/serialization/ndjson/test_relationship.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,82 @@ def test_bidirectional_relationship():
192192
)
193193
assert rel_serialized["relationship"]["type"] == "bidirectional"
194194
assert rel_2_serialized["relationship"]["type"] == "bidirectional"
195+
196+
197+
def test_readonly_relationships():
198+
ner_source = ObjectAnnotation(
199+
name="e1",
200+
value=TextEntity(start=10, end=12),
201+
)
202+
203+
ner_target = ObjectAnnotation(
204+
name="e2",
205+
value=TextEntity(start=30, end=35),
206+
)
207+
208+
# Test unidirectional relationship with readonly=True
209+
readonly_relationship = RelationshipAnnotation(
210+
name="readonly_rel",
211+
value=Relationship(
212+
source=ner_source,
213+
target=ner_target,
214+
type=Relationship.Type.UNIDIRECTIONAL,
215+
readonly=True,
216+
),
217+
)
218+
219+
# Test bidirectional relationship with readonly=False
220+
non_readonly_relationship = RelationshipAnnotation(
221+
name="non_readonly_rel",
222+
value=Relationship(
223+
source=ner_source,
224+
target=ner_target,
225+
type=Relationship.Type.BIDIRECTIONAL,
226+
readonly=False,
227+
),
228+
)
229+
230+
label = Label(
231+
data={"uid": "clqbkpy236syk07978v3pscw1"},
232+
annotations=[
233+
ner_source,
234+
ner_target,
235+
readonly_relationship,
236+
non_readonly_relationship,
237+
],
238+
)
239+
240+
serialized_label = list(NDJsonConverter.serialize([label]))
241+
242+
ner_source_serialized = next(
243+
annotation
244+
for annotation in serialized_label
245+
if annotation["name"] == ner_source.name
246+
)
247+
ner_target_serialized = next(
248+
annotation
249+
for annotation in serialized_label
250+
if annotation["name"] == ner_target.name
251+
)
252+
readonly_rel_serialized = next(
253+
annotation
254+
for annotation in serialized_label
255+
if annotation["name"] == readonly_relationship.name
256+
)
257+
non_readonly_rel_serialized = next(
258+
annotation
259+
for annotation in serialized_label
260+
if annotation["name"] == non_readonly_relationship.name
261+
)
262+
263+
# Verify readonly relationship
264+
assert readonly_rel_serialized["relationship"]["source"] == ner_source_serialized["uuid"]
265+
assert readonly_rel_serialized["relationship"]["target"] == ner_target_serialized["uuid"]
266+
assert readonly_rel_serialized["relationship"]["type"] == "unidirectional"
267+
assert readonly_rel_serialized["relationship"]["readonly"] is True
268+
269+
# Verify non-readonly relationship
270+
assert non_readonly_rel_serialized["relationship"]["source"] == ner_source_serialized["uuid"]
271+
assert non_readonly_rel_serialized["relationship"]["target"] == ner_target_serialized["uuid"]
272+
assert non_readonly_rel_serialized["relationship"]["type"] == "bidirectional"
273+
assert non_readonly_rel_serialized["relationship"]["readonly"] is False

0 commit comments

Comments
 (0)