Skip to content

Commit 7e2f1fd

Browse files
authored
[SDK-0] Fix class annotations (#1349)
1 parent 632d64d commit 7e2f1fd

File tree

9 files changed

+51
-18
lines changed

9 files changed

+51
-18
lines changed
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import abc
2-
from uuid import UUID, uuid4
3-
from typing import Any, Dict
2+
from uuid import UUID
3+
from typing import Any, Dict, Optional
44
from pydantic import PrivateAttr
55

66
from .feature import FeatureSchema
@@ -9,10 +9,10 @@
99
class BaseAnnotation(FeatureSchema, abc.ABC):
1010
""" Base annotation class. Shouldn't be directly instantiated
1111
"""
12-
_uuid: UUID = PrivateAttr()
12+
_uuid: Optional[UUID] = PrivateAttr()
1313
extra: Dict[str, Any] = {}
1414

1515
def __init__(self, **data):
1616
super().__init__(**data)
1717
extra_uuid = data.get("extra", {}).get("uuid")
18-
self._uuid = data.get("_uuid") or extra_uuid or uuid4()
18+
self._uuid = data.get("_uuid") or extra_uuid or None

labelbox/data/serialization/ndjson/converter.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import logging
2+
import uuid
23
from typing import Any, Dict, Generator, Iterable
34

45
from ...annotation_types.collection import LabelCollection, LabelGenerator
6+
from ...annotation_types.relationship import RelationshipAnnotation
57
from .label import NDLabel
68

79
logger = logging.getLogger(__name__)
@@ -21,7 +23,7 @@ def deserialize(json_data: Iterable[Dict[str, Any]]) -> LabelGenerator:
2123
Returns:
2224
LabelGenerator containing the ndjson data.
2325
"""
24-
data = NDLabel(**{'annotations': json_data})
26+
data = NDLabel(**{"annotations": json_data})
2527
res = data.to_common()
2628
return res
2729

@@ -40,8 +42,50 @@ def serialize(
4042
Returns:
4143
A generator for accessing the ndjson representation of the data
4244
"""
45+
used_annotation_uuids = set()
46+
for label in labels:
47+
annotation_uuid_to_generated_uuid_lookup = {}
48+
# UUIDs are private properties used to enhance UX when defining relationships.
49+
# They are created for all annotations, but only utilized for relationships.
50+
# To avoid overwriting, UUIDs must be unique across labels.
51+
# Non-relationship annotation UUIDs are dropped (server-side generation will occur).
52+
# For relationship annotations, new UUIDs are generated and stored in a lookup table.
53+
for annotation in label.annotations:
54+
if isinstance(annotation, RelationshipAnnotation):
55+
source_uuid = annotation.value.source._uuid
56+
target_uuid = annotation.value.target._uuid
57+
58+
if (len(
59+
used_annotation_uuids.intersection(
60+
{source_uuid, target_uuid})) > 0):
61+
new_source_uuid = uuid.uuid4()
62+
new_target_uuid = uuid.uuid4()
63+
64+
annotation_uuid_to_generated_uuid_lookup[
65+
source_uuid] = new_source_uuid
66+
annotation_uuid_to_generated_uuid_lookup[
67+
target_uuid] = new_target_uuid
68+
annotation.value.source._uuid = new_source_uuid
69+
annotation.value.target._uuid = new_target_uuid
70+
else:
71+
annotation_uuid_to_generated_uuid_lookup[
72+
source_uuid] = source_uuid
73+
annotation_uuid_to_generated_uuid_lookup[
74+
target_uuid] = target_uuid
75+
used_annotation_uuids.add(annotation._uuid)
76+
77+
for annotation in label.annotations:
78+
if (not isinstance(annotation, RelationshipAnnotation) and
79+
hasattr(annotation, "_uuid")):
80+
annotation._uuid = annotation_uuid_to_generated_uuid_lookup.get(
81+
annotation._uuid, annotation._uuid)
82+
4383
for example in NDLabel.from_common(labels):
44-
res = example.dict(by_alias=True)
84+
annotation_uuid = getattr(example, "uuid", None)
85+
86+
res = example.dict(
87+
by_alias=True,
88+
exclude={"uuid"} if annotation_uuid == "None" else None)
4589
for k, v in list(res.items()):
4690
if k in IGNORE_IF_NONE and v is None:
4791
del res[k]

tests/data/serialization/ndjson/test_checklist.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ def test_serialization_min():
3333
serialized = NDJsonConverter.serialize([label])
3434
res = next(serialized)
3535

36-
res.pop("uuid")
3736
assert res == expected
3837

3938
deserialized = NDJsonConverter.deserialize([res])
@@ -113,7 +112,6 @@ def test_serialization_with_classification():
113112
serialized = NDJsonConverter.serialize([label])
114113
res = next(serialized)
115114

116-
res.pop("uuid")
117115
assert res == expected
118116

119117
deserialized = NDJsonConverter.deserialize([res])
@@ -197,7 +195,6 @@ def test_serialization_with_classification_double_nested():
197195
serialized = NDJsonConverter.serialize([label])
198196
res = next(serialized)
199197

200-
res.pop("uuid")
201198
assert res == expected
202199

203200
deserialized = NDJsonConverter.deserialize([res])
@@ -277,7 +274,6 @@ def test_serialization_with_classification_double_nested_2():
277274

278275
serialized = NDJsonConverter.serialize([label])
279276
res = next(serialized)
280-
res.pop("uuid")
281277
assert res == expected
282278

283279
deserialized = NDJsonConverter.deserialize([res])

tests/data/serialization/ndjson/test_conversation.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@
8383
[free_text_label, free_text_ndjson]])
8484
def test_message_based_radio_classification(label, ndjson):
8585
serialized_label = list(NDJsonConverter().serialize(label))
86-
serialized_label[0].pop('uuid')
8786
assert serialized_label == ndjson
8887

8988
deserialized_label = list(NDJsonConverter().deserialize(ndjson))

tests/data/serialization/ndjson/test_document.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ def test_pdf_with_name_only():
6969

7070
def test_pdf_bbox_serialize():
7171
serialized = list(NDJsonConverter.serialize(bbox_labels))
72-
serialized[0].pop('uuid')
7372
assert serialized == bbox_ndjson
7473

7574

tests/data/serialization/ndjson/test_image.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ def test_mask_from_arr():
8585
],
8686
data=ImageData(uid="0" * 25))
8787
res = next(NDJsonConverter.serialize([label]))
88-
res.pop('uuid')
8988
assert res == {
9089
"classifications": [],
9190
"schemaId": "1" * 25,

tests/data/serialization/ndjson/test_radio.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ def test_serialization_with_radio_min():
3434
serialized = NDJsonConverter.serialize([label])
3535
res = next(serialized)
3636

37-
res.pop("uuid")
3837
assert res == expected
3938

4039
deserialized = NDJsonConverter.deserialize([res])
@@ -86,7 +85,6 @@ def test_serialization_with_radio_classification():
8685

8786
serialized = NDJsonConverter.serialize([label])
8887
res = next(serialized)
89-
res.pop("uuid")
9088
assert res == expected
9189

9290
deserialized = NDJsonConverter.deserialize([res])

tests/data/serialization/ndjson/test_video.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,6 @@ def test_video_classification_global_subclassifications():
8787

8888
serialized = NDJsonConverter.serialize([label])
8989
res = [x for x in serialized]
90-
for annotations in res:
91-
annotations.pop("uuid")
9290
assert res == [expected_first_annotation, expected_second_annotation]
9391

9492
deserialized = NDJsonConverter.deserialize(res)

tests/integration/annotation_import/test_data_types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
def get_annotation_comparison_dicts_from_labels(labels):
6060
labels_ndjson = list(NDJsonConverter.serialize(labels))
6161
for annotation in labels_ndjson:
62-
annotation.pop('uuid')
62+
annotation.pop('uuid', None)
6363
annotation.pop('dataRow')
6464

6565
if 'masks' in annotation:

0 commit comments

Comments
 (0)