Skip to content

Commit 8fd6bd1

Browse files
feat(project): Add parameter to Project.clear to delete project (#1322)
`project.clear(delete_project=True)` now deletes the entire project, while `project.clear(delete_project=False)` (the default) removes every item from the project. Closes #1294
1 parent a1ef819 commit 8fd6bd1

File tree

3 files changed

+75
-2
lines changed

3 files changed

+75
-2
lines changed

โ€Žskore/src/skore/project/project.pyโ€Ž

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
import functools
6+
import shutil
57
from collections.abc import Iterator
68
from logging import INFO, NullHandler, getLogger
79
from pathlib import Path
@@ -16,6 +18,30 @@
1618
logger.setLevel(INFO)
1719

1820

21+
class ProjectDeletedError(Exception):
22+
"""A method of a Project was called but the Project is marked as deleted."""
23+
24+
25+
def _raise_if_deleted(method):
26+
"""Raise if the underlying Project has been deleted, otherwise execute `method`.
27+
28+
This wrapper makes it safe to "delete" a Project, even if the Project instance
29+
still exists.
30+
"""
31+
32+
@functools.wraps(method)
33+
def wrapper(self, *args, **kwargs):
34+
if self._storage_initialized is not True:
35+
raise ProjectDeletedError(
36+
"This Project instance is marked as deleted. "
37+
"Please re-create a Project and discard the current one."
38+
)
39+
40+
return method(self, *args, **kwargs)
41+
42+
return wrapper
43+
44+
1945
class Project:
2046
"""
2147
A collection of items persisted in a storage.
@@ -95,16 +121,38 @@ def __init__(
95121
# Initialize repositories with dedicated storages
96122
self._item_repository = ItemRepository(DiskCacheStorage(item_storage_dirpath))
97123

124+
self._storage_initialized = True
125+
98126
# Check if the project should rejoin a server
99127
from skore.project._launch import ServerInfo # avoid circular import
100128

101129
self._server_info = ServerInfo.rejoin(self)
102130

103-
def clear(self):
104-
"""Clear the project."""
131+
@_raise_if_deleted
132+
def clear(self, delete_project=False):
133+
"""Remove all items from the project.
134+
135+
.. warning::
136+
Clearing the project with `delete_project=True` will invalidate the whole
137+
`Project` instance, making it unusable.
138+
A new Project instance can be created using the :class:`skore.Project`
139+
constructor or the :func:`skore.open` function.
140+
141+
Parameters
142+
----------
143+
delete_project : bool
144+
If set, the project will be deleted entirely.
145+
"""
146+
if delete_project:
147+
self._storage_initialized = False
148+
del self._item_repository
149+
shutil.rmtree(self.path)
150+
return
151+
105152
for item_key in self._item_repository:
106153
self._item_repository.delete_item(item_key)
107154

155+
@_raise_if_deleted
108156
def put(
109157
self,
110158
key: str,
@@ -150,6 +198,7 @@ def put(
150198
),
151199
)
152200

201+
@_raise_if_deleted
153202
def get(
154203
self,
155204
key: str,
@@ -211,6 +260,7 @@ def dto(item):
211260

212261
raise ValueError('`version` should be -1, "all", or an integer')
213262

263+
@_raise_if_deleted
214264
def keys(self) -> list[str]:
215265
"""
216266
Get all keys of items stored in the project.
@@ -222,6 +272,7 @@ def keys(self) -> list[str]:
222272
"""
223273
return self._item_repository.keys()
224274

275+
@_raise_if_deleted
225276
def __iter__(self) -> Iterator[str]:
226277
"""
227278
Yield the keys of items stored in the project.
@@ -233,6 +284,7 @@ def __iter__(self) -> Iterator[str]:
233284
"""
234285
yield from self._item_repository
235286

287+
@_raise_if_deleted
236288
def delete(self, key: str):
237289
"""Delete the item corresponding to ``key`` from the Project.
238290
@@ -248,6 +300,7 @@ def delete(self, key: str):
248300
"""
249301
self._item_repository.delete_item(key)
250302

303+
@_raise_if_deleted
251304
def set_note(self, key: str, note: str, *, version=-1):
252305
"""Attach a note to key ``key``.
253306
@@ -277,6 +330,7 @@ def set_note(self, key: str, note: str, *, version=-1):
277330
"""
278331
return self._item_repository.set_item_note(key=key, note=note, version=version)
279332

333+
@_raise_if_deleted
280334
def get_note(self, key: str, *, version=-1) -> Union[str, None]:
281335
"""Retrieve a note previously attached to key ``key``.
282336
@@ -306,6 +360,7 @@ def get_note(self, key: str, *, version=-1) -> Union[str, None]:
306360
"""
307361
return self._item_repository.get_item_note(key=key, version=version)
308362

363+
@_raise_if_deleted
309364
def delete_note(self, key: str, *, version=-1):
310365
"""Delete a note previously attached to key ``key``.
311366
@@ -333,6 +388,7 @@ def delete_note(self, key: str, *, version=-1):
333388
"""
334389
return self._item_repository.delete_item_note(key=key, version=version)
335390

391+
@_raise_if_deleted
336392
def shutdown_web_ui(self):
337393
"""Shutdown the web UI server if it is running."""
338394
if self._server_info is None:

โ€Žskore/tests/conftest.pyโ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def in_memory_project(monkeypatch):
4343
project.path = None
4444
project.name = "test"
4545
project._item_repository = ItemRepository(storage=InMemoryStorage())
46+
project._storage_initialized = True
4647

4748
return project
4849

โ€Žskore/tests/unit/project/test_project.pyโ€Ž

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from PIL import Image
1313
from sklearn.ensemble import RandomForestClassifier
1414
from skore import Project
15+
from skore.project.project import ProjectDeletedError
1516

1617

1718
@pytest.fixture(autouse=True)
@@ -49,6 +50,21 @@ def test_clear(tmp_path):
4950
assert project.keys() == []
5051
assert project._item_repository.keys() == []
5152

53+
assert dirpath.exists()
54+
55+
56+
def test_clear_delete_project(tmp_path):
57+
dirpath = tmp_path / "my-project.skore"
58+
project = Project(dirpath)
59+
60+
project.clear(delete_project=True)
61+
assert not dirpath.exists()
62+
63+
with pytest.raises(
64+
ProjectDeletedError, match="This Project instance is marked as deleted"
65+
):
66+
project.keys()
67+
5268

5369
def test_put_string_item(in_memory_project):
5470
in_memory_project.put("string_item", "Hello, World!")

0 commit comments

Comments
ย (0)