Skip to content

Commit dae1739

Browse files
committed
[ENH]: Make detach a Collection method
1 parent c8dc1fe commit dae1739

File tree

20 files changed

+207
-272
lines changed

20 files changed

+207
-272
lines changed

chromadb/api/__init__.py

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -868,7 +868,7 @@ def get_attached_function(
868868
@abstractmethod
869869
def detach_function(
870870
self,
871-
attached_function_id: UUID,
871+
name: str,
872872
input_collection_id: UUID,
873873
delete_output: bool = False,
874874
tenant: str = DEFAULT_TENANT,
@@ -877,7 +877,7 @@ def detach_function(
877877
"""Detach a function and prevent any further runs.
878878
879879
Args:
880-
attached_function_id: ID of the attached function to remove
880+
name: Name of the attached function to remove
881881
input_collection_id: ID of the input collection
882882
delete_output: Whether to also delete the output collection
883883
tenant: The tenant name
@@ -887,27 +887,3 @@ def detach_function(
887887
bool: True if successful
888888
"""
889889
pass
890-
891-
@abstractmethod
892-
def get_attached_function(
893-
self,
894-
name: str,
895-
input_collection_id: UUID,
896-
tenant: str = DEFAULT_TENANT,
897-
database: str = DEFAULT_DATABASE,
898-
) -> "AttachedFunction":
899-
"""Get an attached function by name for a specific collection.
900-
901-
Args:
902-
name: Name of the attached function
903-
input_collection_id: ID of the input collection
904-
tenant: The tenant name
905-
database: The database name
906-
907-
Returns:
908-
AttachedFunction: The attached function object
909-
910-
Raises:
911-
NotFoundError: If the attached function doesn't exist
912-
"""
913-
pass

chromadb/api/fastapi.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -830,7 +830,7 @@ def get_attached_function(
830830
@override
831831
def detach_function(
832832
self,
833-
attached_function_id: UUID,
833+
name: str,
834834
input_collection_id: UUID,
835835
delete_output: bool = False,
836836
tenant: str = DEFAULT_TENANT,
@@ -839,10 +839,9 @@ def detach_function(
839839
"""Detach a function and prevent any further runs."""
840840
resp_json = self._make_request(
841841
"post",
842-
f"/tenants/{tenant}/databases/{database}/attached_functions/{attached_function_id}/detach",
842+
f"/tenants/{tenant}/databases/{database}/collections/{input_collection_id}/attached_functions/{name}/detach",
843843
json={
844844
"delete_output": delete_output,
845-
"input_collection_id": str(input_collection_id),
846845
},
847846
)
848847
return cast(bool, resp_json["success"])

chromadb/api/models/AttachedFunction.py

Lines changed: 9 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -77,40 +77,21 @@ def params(self) -> Optional[Dict[str, Any]]:
7777
@staticmethod
7878
def _normalize_params(params: Optional[Any]) -> Dict[str, Any]:
7979
"""Normalize params to a consistent dict format.
80-
80+
8181
Handles None, empty strings, JSON strings, and dicts.
8282
"""
8383
if params is None:
8484
return {}
8585
if isinstance(params, str):
8686
try:
87-
return json.loads(params) if params else {}
87+
result = json.loads(params) if params else {}
88+
return result if isinstance(result, dict) else {}
8889
except json.JSONDecodeError:
8990
return {}
9091
if isinstance(params, dict):
9192
return params
9293
return {}
9394

94-
def detach(self, delete_output_collection: bool = False) -> bool:
95-
"""Detach this function and prevent any further runs.
96-
97-
Args:
98-
delete_output_collection: Whether to also delete the output collection. Defaults to False.
99-
100-
Returns:
101-
bool: True if successful
102-
103-
Example:
104-
>>> success = attached_fn.detach(delete_output_collection=True)
105-
"""
106-
return self._client.detach_function(
107-
attached_function_id=self._id,
108-
input_collection_id=self._input_collection_id,
109-
delete_output=delete_output_collection,
110-
tenant=self._tenant,
111-
database=self._database,
112-
)
113-
11495
def __repr__(self) -> str:
11596
return (
11697
f"AttachedFunction(id={self._id}, name='{self._name}', "
@@ -123,11 +104,11 @@ def __eq__(self, other: object) -> bool:
123104
"""Compare two AttachedFunction objects for equality."""
124105
if not isinstance(other, AttachedFunction):
125106
return False
126-
107+
127108
# Normalize params: handle None, {}, and JSON strings
128109
self_params = self._normalize_params(self._params)
129110
other_params = self._normalize_params(other._params)
130-
111+
131112
return (
132113
self._id == other._id
133114
and self._name == other._name
@@ -143,8 +124,10 @@ def __hash__(self) -> int:
143124
"""Return hash of the AttachedFunction."""
144125
# Normalize params using the same logic as __eq__
145126
normalized_params = self._normalize_params(self._params)
146-
params_tuple = tuple(sorted(normalized_params.items())) if normalized_params else ()
147-
127+
params_tuple = (
128+
tuple(sorted(normalized_params.items())) if normalized_params else ()
129+
)
130+
148131
return hash(
149132
(
150133
self._id,

chromadb/api/models/Collection.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,3 +552,28 @@ def get_attached_function(self, name: str) -> "AttachedFunction":
552552
tenant=self.tenant,
553553
database=self.database,
554554
)
555+
556+
def detach_function(
557+
self,
558+
name: str,
559+
delete_output_collection: bool = False,
560+
) -> bool:
561+
"""Detach a function from this collection.
562+
563+
Args:
564+
name: The name of the attached function
565+
delete_output_collection: Whether to also delete the output collection. Defaults to False.
566+
567+
Returns:
568+
bool: True if successful
569+
570+
Example:
571+
>>> success = collection.detach_function("my_function", delete_output_collection=True)
572+
"""
573+
return self._client.detach_function(
574+
name=name,
575+
input_collection_id=self.id,
576+
delete_output=delete_output_collection,
577+
tenant=self.tenant,
578+
database=self.database,
579+
)

chromadb/api/rust.py

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,7 @@ def get_attached_function(
637637
@override
638638
def detach_function(
639639
self,
640-
attached_function_id: UUID,
640+
name: str,
641641
input_collection_id: UUID,
642642
delete_output: bool = False,
643643
tenant: str = DEFAULT_TENANT,
@@ -649,20 +649,6 @@ def detach_function(
649649
"The Rust bindings (embedded mode) do not support attached function operations."
650650
)
651651

652-
@override
653-
def get_attached_function(
654-
self,
655-
name: str,
656-
input_collection_id: UUID,
657-
tenant: str = DEFAULT_TENANT,
658-
database: str = DEFAULT_DATABASE,
659-
) -> "AttachedFunction":
660-
"""Attached functions are not supported in the Rust bindings (local embedded mode)."""
661-
raise NotImplementedError(
662-
"Attached functions are only supported when connecting to a Chroma server via HttpClient. "
663-
"The Rust bindings (embedded mode) do not support attached function operations."
664-
)
665-
666652
# TODO: Remove this if it's not planned to be used
667653
@override
668654
def get_user_identity(self) -> UserIdentity:

chromadb/api/segment.py

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -946,7 +946,7 @@ def get_attached_function(
946946
@override
947947
def detach_function(
948948
self,
949-
attached_function_id: UUID,
949+
name: str,
950950
input_collection_id: UUID,
951951
delete_output: bool = False,
952952
tenant: str = DEFAULT_TENANT,
@@ -958,20 +958,6 @@ def detach_function(
958958
"The Segment API (embedded mode) does not support attached function operations."
959959
)
960960

961-
@override
962-
def get_attached_function(
963-
self,
964-
name: str,
965-
input_collection_id: UUID,
966-
tenant: str = DEFAULT_TENANT,
967-
database: str = DEFAULT_DATABASE,
968-
) -> "AttachedFunction":
969-
"""Attached functions are not supported in the Segment API (local embedded mode)."""
970-
raise NotImplementedError(
971-
"Attached functions are only supported when connecting to a Chroma server via HttpClient. "
972-
"The Segment API (embedded mode) does not support attached function operations."
973-
)
974-
975961
# TODO: This could potentially cause race conditions in a distributed version of the
976962
# system, since the cache is only local.
977963
# TODO: promote collection -> topic to a base class method so that it can be

chromadb/test/distributed/test_task_api.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ def test_count_function_attach_and_detach(basic_http_client: System) -> None:
5757
assert result["metadatas"][0]["total_count"] == 300
5858

5959
# Remove the task
60-
success = attached_fn.detach(
60+
success = collection.detach_function(
61+
attached_fn.name,
6162
delete_output_collection=True,
6263
)
6364

@@ -108,7 +109,7 @@ def test_attach_function_returns_function_name(basic_http_client: System) -> Non
108109
assert retrieved_fn == attached_fn
109110

110111
# Clean up
111-
attached_fn.detach(delete_output_collection=True)
112+
collection.detach_function(attached_fn.name, delete_output_collection=True)
112113

113114

114115
def test_function_multiple_collections(basic_http_client: System) -> None:
@@ -146,8 +147,14 @@ def test_function_multiple_collections(basic_http_client: System) -> None:
146147
assert attached_fn1.id != attached_fn2.id
147148

148149
# Clean up
149-
assert attached_fn1.detach(delete_output_collection=True) is True
150-
assert attached_fn2.detach(delete_output_collection=True) is True
150+
assert (
151+
collection1.detach_function(attached_fn1.name, delete_output_collection=True)
152+
is True
153+
)
154+
assert (
155+
collection2.detach_function(attached_fn2.name, delete_output_collection=True)
156+
is True
157+
)
151158

152159

153160
def test_functions_one_attached_function_per_collection(
@@ -191,7 +198,10 @@ def test_functions_one_attached_function_per_collection(
191198
)
192199

193200
# Detach the first function
194-
assert attached_fn1.detach(delete_output_collection=True) is True
201+
assert (
202+
collection.detach_function(attached_fn1.name, delete_output_collection=True)
203+
is True
204+
)
195205

196206
# Now we should be able to attach a new function
197207
attached_fn2 = collection.attach_function(
@@ -205,7 +215,10 @@ def test_functions_one_attached_function_per_collection(
205215
assert attached_fn2.id != attached_fn1.id
206216

207217
# Clean up
208-
assert attached_fn2.detach(delete_output_collection=True) is True
218+
assert (
219+
collection.detach_function(attached_fn2.name, delete_output_collection=True)
220+
is True
221+
)
209222

210223

211224
def test_function_remove_nonexistent(basic_http_client: System) -> None:
@@ -222,8 +235,8 @@ def test_function_remove_nonexistent(basic_http_client: System) -> None:
222235
params=None,
223236
)
224237

225-
attached_fn.detach(delete_output_collection=True)
238+
collection.detach_function(attached_fn.name, delete_output_collection=True)
226239

227240
# Trying to detach this function again should raise NotFoundError
228241
with pytest.raises(NotFoundError, match="does not exist"):
229-
attached_fn.detach(delete_output_collection=True)
242+
collection.detach_function(attached_fn.name, delete_output_collection=True)

0 commit comments

Comments
 (0)