Skip to content

Commit f433033

Browse files
committed
Refactor, add unit tests
1 parent 9097bfd commit f433033

File tree

3 files changed

+280
-50
lines changed

3 files changed

+280
-50
lines changed

libs/labelbox/src/labelbox/schema/ontology.py

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -155,56 +155,6 @@ def add_classification(self, classification: Classification) -> None:
155155
)
156156
self.classifications.append(classification)
157157

158-
@dataclass
159-
class RelationshipTool(Tool):
160-
"""
161-
A relationship tool to be added to a Project's ontology.
162-
163-
The "tool" parameter is automatically set to Tool.Type.RELATIONSHIP
164-
and doesn't need to be passed during instantiation.
165-
166-
The "classifications" parameter holds a list of Classification objects.
167-
This can be used to add nested classifications to a tool.
168-
169-
Example(s):
170-
tool = RelationshipTool(
171-
name = "Relationship Tool example",
172-
constraints = [
173-
("source_tool_feature_schema_id_1", "target_tool_feature_schema_id_1"),
174-
("source_tool_feature_schema_id_2", "target_tool_feature_schema_id_2")
175-
]
176-
)
177-
classification = Classification(
178-
class_type = Classification.Type.TEXT,
179-
instructions = "Classification Example")
180-
tool.add_classification(classification)
181-
182-
Attributes:
183-
tool: Tool.Type.RELATIONSHIP (automatically set)
184-
name: (str)
185-
required: (bool)
186-
color: (str)
187-
classifications: (list)
188-
schema_id: (str)
189-
feature_schema_id: (str)
190-
attributes: (list)
191-
constraints: (list of [str, str])
192-
"""
193-
194-
constraints: Optional[List[Tuple[str, str]]] = None
195-
196-
def __post_init__(self):
197-
# Ensure tool type is set to RELATIONSHIP
198-
self.tool = Tool.Type.RELATIONSHIP
199-
super().__post_init__()
200-
201-
def asdict(self) -> Dict[str, Any]:
202-
result = super().asdict()
203-
if self.constraints is not None:
204-
result["constraints"] = self.constraints
205-
return result
206-
207-
208158
"""
209159
The following 2 functions help to bridge the gap between the step reasoning all other tool ontologies.
210160
"""
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# type: ignore
2+
3+
import uuid
4+
from dataclasses import dataclass
5+
from typing import Any, Dict, List, Optional, Tuple
6+
7+
from labelbox.schema.ontology import Tool
8+
9+
@dataclass
10+
class RelationshipTool(Tool):
11+
"""
12+
A relationship tool to be added to a Project's ontology.
13+
14+
The "tool" parameter is automatically set to Tool.Type.RELATIONSHIP
15+
and doesn't need to be passed during instantiation.
16+
17+
The "classifications" parameter holds a list of Classification objects.
18+
This can be used to add nested classifications to a tool.
19+
20+
Example(s):
21+
tool = RelationshipTool(
22+
name = "Relationship Tool example",
23+
constraints = [
24+
("source_tool_feature_schema_id_1", "target_tool_feature_schema_id_1"),
25+
("source_tool_feature_schema_id_2", "target_tool_feature_schema_id_2")
26+
]
27+
)
28+
classification = Classification(
29+
class_type = Classification.Type.TEXT,
30+
instructions = "Classification Example")
31+
tool.add_classification(classification)
32+
33+
Attributes:
34+
tool: Tool.Type.RELATIONSHIP (automatically set)
35+
name: (str)
36+
required: (bool)
37+
color: (str)
38+
classifications: (list)
39+
schema_id: (str)
40+
feature_schema_id: (str)
41+
attributes: (list)
42+
constraints: (list of [str, str])
43+
"""
44+
45+
constraints: Optional[List[Tuple[str, str]]] = None
46+
47+
def __init__(self, name: str, constraints: Optional[List[Tuple[str, str]]] = None, **kwargs):
48+
super().__init__(Tool.Type.RELATIONSHIP, name, **kwargs)
49+
if constraints is not None:
50+
self.constraints = constraints
51+
52+
def __post_init__(self):
53+
# Ensure tool type is set to RELATIONSHIP
54+
self.tool = Tool.Type.RELATIONSHIP
55+
super().__post_init__()
56+
57+
def asdict(self) -> Dict[str, Any]:
58+
result = super().asdict()
59+
if self.constraints is not None:
60+
result["definition"] = { "constraints": self.constraints }
61+
return result
62+
63+
def add_constraint(self, start: Tool, end: Tool) -> None:
64+
if self.constraints is None:
65+
self.constraints = []
66+
67+
# Ensure feature schema ids are set for the tools,
68+
# the newly set ids will be changed during ontology creation
69+
# but we need to refer to the same ids in the constraints array
70+
# to ensure that the valid constraints are created.
71+
if start.feature_schema_id is None:
72+
start.feature_schema_id = str(uuid.uuid4())
73+
if start.schema_id is None:
74+
start.schema_id = str(uuid.uuid4())
75+
if end.feature_schema_id is None:
76+
end.feature_schema_id = str(uuid.uuid4())
77+
if end.schema_id is None:
78+
end.schema_id = str(uuid.uuid4())
79+
80+
self.constraints.append((start.feature_schema_id, end.feature_schema_id))
81+
82+
def set_constraints(self, constraints: List[Tuple[Tool, Tool]]) -> None:
83+
self.constraints = []
84+
for constraint in constraints:
85+
self.add_constraint(constraint[0], constraint[1])
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import pytest
2+
import uuid
3+
from unittest.mock import patch
4+
5+
from labelbox.schema.ontology import Tool
6+
from labelbox.schema.tool_building.relationship_tool import RelationshipTool
7+
from labelbox.schema.tool_building.classification import Classification
8+
9+
10+
def test_basic_instantiation():
11+
tool = RelationshipTool(name="Test Relationship Tool")
12+
13+
assert tool.name == "Test Relationship Tool"
14+
assert tool.tool == Tool.Type.RELATIONSHIP
15+
assert tool.constraints is None
16+
assert tool.required is False
17+
assert tool.color is None
18+
assert tool.schema_id is None
19+
assert tool.feature_schema_id is None
20+
21+
22+
def test_instantiation_with_constraints():
23+
constraints = [
24+
("source_id_1", "target_id_1"),
25+
("source_id_2", "target_id_2")
26+
]
27+
tool = RelationshipTool(name="Test Tool", constraints=constraints)
28+
29+
assert tool.name == "Test Tool"
30+
assert tool.constraints == constraints
31+
assert len(tool.constraints) == 2
32+
33+
def test_post_init_sets_tool_type():
34+
tool = RelationshipTool(name="Test Tool")
35+
assert tool.tool == Tool.Type.RELATIONSHIP
36+
37+
38+
def test_asdict_without_constraints():
39+
tool = RelationshipTool(
40+
name="Test Tool",
41+
required=True,
42+
color="#FF0000"
43+
)
44+
45+
result = tool.asdict()
46+
expected = {
47+
"tool": "edge",
48+
"name": "Test Tool",
49+
"required": True,
50+
"color": "#FF0000",
51+
"classifications": [],
52+
"schemaNodeId": None,
53+
"featureSchemaId": None,
54+
"attributes": None
55+
}
56+
57+
assert result == expected
58+
59+
def test_asdict_with_constraints():
60+
constraints = [("source_id", "target_id")]
61+
tool = RelationshipTool(name="Test Tool", constraints=constraints)
62+
63+
result = tool.asdict()
64+
65+
assert "definition" in result
66+
assert result["definition"] == {"constraints": constraints}
67+
assert result["tool"] == "edge"
68+
assert result["name"] == "Test Tool"
69+
70+
71+
def test_add_constraint_to_empty_constraints():
72+
tool = RelationshipTool(name="Test Tool")
73+
start_tool = Tool(Tool.Type.BBOX, "Start Tool")
74+
end_tool = Tool(Tool.Type.POLYGON, "End Tool")
75+
76+
with patch('uuid.uuid4') as mock_uuid:
77+
mock_uuid.return_value.hex = "test-uuid"
78+
tool.add_constraint(start_tool, end_tool)
79+
80+
assert tool.constraints is not None
81+
assert len(tool.constraints) == 1
82+
assert start_tool.feature_schema_id is not None
83+
assert start_tool.schema_id is not None
84+
assert end_tool.feature_schema_id is not None
85+
assert end_tool.schema_id is not None
86+
87+
88+
def test_add_constraint_to_existing_constraints():
89+
existing_constraints = [("existing_source", "existing_target")]
90+
tool = RelationshipTool(name="Test Tool", constraints=existing_constraints)
91+
92+
start_tool = Tool(Tool.Type.BBOX, "Start Tool")
93+
end_tool = Tool(Tool.Type.POLYGON, "End Tool")
94+
95+
tool.add_constraint(start_tool, end_tool)
96+
97+
assert len(tool.constraints) == 2
98+
assert tool.constraints[0] == ("existing_source", "existing_target")
99+
assert tool.constraints[1] == (start_tool.feature_schema_id, end_tool.feature_schema_id)
100+
101+
102+
def test_add_constraint_preserves_existing_ids():
103+
tool = RelationshipTool(name="Test Tool")
104+
start_tool_feature_schema_id = "start_tool_feature_schema_id"
105+
start_tool_schema_id = "start_tool_schema_id"
106+
start_tool = Tool(Tool.Type.BBOX, "Start Tool", feature_schema_id=start_tool_feature_schema_id, schema_id=start_tool_schema_id)
107+
end_tool_feature_schema_id = "end_tool_feature_schema_id"
108+
end_tool_schema_id = "end_tool_schema_id"
109+
end_tool = Tool(Tool.Type.POLYGON, "End Tool", feature_schema_id=end_tool_feature_schema_id, schema_id=end_tool_schema_id)
110+
111+
tool.add_constraint(start_tool, end_tool)
112+
113+
assert start_tool.feature_schema_id == start_tool_feature_schema_id
114+
assert start_tool.schema_id == start_tool_schema_id
115+
assert end_tool.feature_schema_id == end_tool_feature_schema_id
116+
assert end_tool.schema_id == end_tool_schema_id
117+
assert tool.constraints == [(start_tool_feature_schema_id, end_tool_feature_schema_id)]
118+
119+
120+
def test_set_constraints():
121+
tool = RelationshipTool(name="Test Tool")
122+
123+
start_tool1 = Tool(Tool.Type.BBOX, "Start Tool 1")
124+
end_tool1 = Tool(Tool.Type.POLYGON, "End Tool 1")
125+
start_tool2 = Tool(Tool.Type.POINT, "Start Tool 2")
126+
end_tool2 = Tool(Tool.Type.LINE, "End Tool 2")
127+
128+
tool.set_constraints([
129+
(start_tool1, end_tool1),
130+
(start_tool2, end_tool2)
131+
])
132+
133+
assert len(tool.constraints) == 2
134+
assert tool.constraints[0] == (start_tool1.feature_schema_id, end_tool1.feature_schema_id)
135+
assert tool.constraints[1] == (start_tool2.feature_schema_id, end_tool2.feature_schema_id)
136+
137+
138+
def test_set_constraints_replaces_existing():
139+
existing_constraints = [("old_source", "old_target")]
140+
tool = RelationshipTool(name="Test Tool", constraints=existing_constraints)
141+
142+
start_tool = Tool(Tool.Type.BBOX, "Start Tool")
143+
end_tool = Tool(Tool.Type.POLYGON, "End Tool")
144+
145+
tool.set_constraints([(start_tool, end_tool)])
146+
147+
assert len(tool.constraints) == 1
148+
assert tool.constraints[0] != ("old_source", "old_target")
149+
assert tool.constraints[0] == (start_tool.feature_schema_id, end_tool.feature_schema_id)
150+
151+
152+
def test_uuid_generation_in_add_constraint():
153+
tool = RelationshipTool(name="Test Tool")
154+
155+
start_tool = Tool(Tool.Type.BBOX, "Start Tool")
156+
end_tool = Tool(Tool.Type.POLYGON, "End Tool")
157+
158+
# Ensure tools don't have IDs initially
159+
assert start_tool.feature_schema_id is None
160+
assert start_tool.schema_id is None
161+
assert end_tool.feature_schema_id is None
162+
assert end_tool.schema_id is None
163+
164+
tool.add_constraint(start_tool, end_tool)
165+
166+
# Check that UUIDs were generated
167+
assert start_tool.feature_schema_id is not None
168+
assert start_tool.schema_id is not None
169+
assert end_tool.feature_schema_id is not None
170+
assert end_tool.schema_id is not None
171+
172+
# Check that they are valid UUID strings
173+
uuid.UUID(start_tool.feature_schema_id) # Will raise ValueError if invalid
174+
uuid.UUID(start_tool.schema_id)
175+
uuid.UUID(end_tool.feature_schema_id)
176+
uuid.UUID(end_tool.schema_id)
177+
178+
179+
def test_constraints_in_asdict():
180+
tool = RelationshipTool(name="Test Tool")
181+
182+
start_tool = Tool(Tool.Type.BBOX, "Start Tool")
183+
end_tool = Tool(Tool.Type.POLYGON, "End Tool")
184+
185+
tool.add_constraint(start_tool, end_tool)
186+
187+
result = tool.asdict()
188+
189+
assert "definition" in result
190+
assert "constraints" in result["definition"]
191+
assert len(result["definition"]["constraints"]) == 1
192+
assert result["definition"]["constraints"][0] == (
193+
start_tool.feature_schema_id,
194+
end_tool.feature_schema_id
195+
)

0 commit comments

Comments
 (0)