Skip to content

Commit 1ed5b6e

Browse files
authored
[SYNPY-1244] Clarify docstrings with delete_permissions function and recursive behavior (#1202)
* Refactor delete_permissions logic to enforce include_container_content with the recursive flag and update unit tests for validation
1 parent f9f0c63 commit 1ed5b6e

File tree

6 files changed

+112
-67
lines changed

6 files changed

+112
-67
lines changed

synapseclient/models/mixins/access_control.py

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -219,8 +219,8 @@ async def main():
219219
async def delete_permissions_async(
220220
self,
221221
include_self: bool = True,
222-
recursive: bool = False,
223222
include_container_content: bool = False,
223+
recursive: bool = False,
224224
target_entity_types: Optional[List[str]] = None,
225225
*,
226226
synapse_client: Optional[Synapse] = None,
@@ -242,13 +242,15 @@ async def delete_permissions_async(
242242
243243
Arguments:
244244
include_self: If True (default), delete the ACL of the current entity.
245-
If False, skip deleting the ACL of the current entity and only process
246-
children if recursive=True or include_container_content=True.
245+
If False, skip deleting the ACL of the current entity.
246+
include_container_content: If True, delete ACLs from contents directly within
247+
containers (files and folders inside self). This must be set to
248+
True for recursive to have any effect. Defaults to False.
247249
recursive: If True and the entity is a container (e.g., Project or Folder),
248-
recursively delete ACLs from all child containers and their children.
250+
recursively process child containers. Note that this must be used with
251+
include_container_content=True to have any effect. Setting recursive=True
252+
with include_container_content=False will raise a ValueError.
249253
Only works on classes that support the `sync_from_synapse_async` method.
250-
include_container_content: If True, delete ACLs from contents directly within
251-
containers (files and folders inside Projects/Folders). Defaults to False.
252254
target_entity_types: Specify which entity types to process when deleting ACLs.
253255
Allowed values are "folder" and "file" (case-insensitive).
254256
If None, defaults to ["folder", "file"].
@@ -292,25 +294,37 @@ async def main():
292294
syn.login()
293295
294296
async def main():
295-
# Delete permissions for this folder and all container entities (folders) within it recursively
296-
await Folder(id="syn123").delete_permissions_async(recursive=True)
297+
# Delete permissions for this folder only (does not affect children)
298+
await Folder(id="syn123").delete_permissions_async()
297299
298-
# Delete permissions for all entities within this folder, but not the folder itself
300+
# Delete permissions for all files and folders directly within this folder,
301+
# but not the folder itself
299302
await Folder(id="syn123").delete_permissions_async(
300303
include_self=False,
301304
include_container_content=True
302305
)
303306
307+
# Delete permissions for all items in the entire hierarchy (folders and their files)
308+
# Both recursive and include_container_content must be True
309+
await Folder(id="syn123").delete_permissions_async(
310+
recursive=True,
311+
include_container_content=True
312+
)
313+
304314
# Delete permissions only for folder entities within this folder recursively
315+
# and their contents
305316
await Folder(id="syn123").delete_permissions_async(
306317
recursive=True,
318+
include_container_content=True,
307319
target_entity_types=["folder"]
308320
)
309321
310-
# Delete all permissions in the entire hierarchy
322+
# Delete permissions only for files within this folder and all subfolders
311323
await Folder(id="syn123").delete_permissions_async(
324+
include_self=False,
312325
recursive=True,
313-
include_container_content=True
326+
include_container_content=True,
327+
target_entity_types=["file"]
314328
)
315329
asyncio.run(main())
316330
```
@@ -327,6 +341,12 @@ async def main():
327341

328342
should_process_children = recursive or include_container_content
329343
if should_process_children and hasattr(self, "sync_from_synapse_async"):
344+
if recursive and not include_container_content:
345+
raise ValueError(
346+
"When recursive=True, include_container_content must also be True. "
347+
"Setting recursive=True with include_container_content=False has no effect."
348+
)
349+
330350
synced = await self._sync_container_structure(client)
331351
if not synced:
332352
return

synapseclient/models/protocols/access_control_protocol.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,9 @@ def set_permissions(
166166

167167
def delete_permissions(
168168
self,
169-
recursive: bool = False,
170169
include_self: bool = True,
171170
include_container_content: bool = False,
171+
recursive: bool = False,
172172
target_entity_types: Optional[List[str]] = None,
173173
*,
174174
synapse_client: Optional[Synapse] = None,
@@ -190,13 +190,15 @@ def delete_permissions(
190190
191191
Arguments:
192192
include_self: If True (default), delete the ACL of the current entity.
193-
If False, skip deleting the ACL of the current entity and only process
194-
children if recursive=True or include_container_content=True.
193+
If False, skip deleting the ACL of the current entity.
194+
include_container_content: If True, delete ACLs from contents directly within
195+
containers (files and folders inside self). This must be set to
196+
True for recursive to have any effect. Defaults to False.
195197
recursive: If True and the entity is a container (e.g., Project or Folder),
196-
recursively delete ACLs from all child containers and their children.
198+
recursively process child containers. Note that this must be used with
199+
include_container_content=True to have any effect. Setting recursive=True
200+
with include_container_content=False will raise a ValueError.
197201
Only works on classes that support the `sync_from_synapse_async` method.
198-
include_container_content: If True, delete ACLs from contents directly within
199-
containers (files and folders inside Projects/Folders). Defaults to False.
200202
target_entity_types: Specify which entity types to process when deleting ACLs.
201203
Allowed values are "folder" and "file" (case-insensitive).
202204
If None, defaults to ["folder", "file"].
@@ -234,25 +236,38 @@ def delete_permissions(
234236
syn = Synapse()
235237
syn.login()
236238
237-
# Delete permissions for this folder and all container entities (folders) within it recursively
238-
Folder(id="syn123").delete_permissions(recursive=True)
239+
# Delete permissions for this folder only (does not affect children)
240+
Folder(id="syn123").delete_permissions()
239241
240-
# Delete permissions for all entities within this folder, but not the folder itself
242+
# Delete permissions for all files and folders directly within this folder,
243+
# but not the folder itself
241244
Folder(id="syn123").delete_permissions(
242245
include_self=False,
243246
include_container_content=True
244247
)
245248
249+
# Delete permissions for all items in the entire hierarchy (folders and their files)
250+
# Both recursive and include_container_content must be True
251+
Folder(id="syn123").delete_permissions(
252+
recursive=True,
253+
include_container_content=True
254+
)
255+
246256
# Delete permissions only for folder entities within this folder recursively
257+
# and their contents
247258
Folder(id="syn123").delete_permissions(
248259
recursive=True,
260+
include_container_content=True,
249261
target_entity_types=["folder"]
250262
)
251263
252-
# Delete all permissions in the entire hierarchy
264+
# Delete permissions only for files within this folder and all subfolders
265+
# Both recursive and include_container_content must be True for this to work
253266
Folder(id="syn123").delete_permissions(
267+
include_self=False,
254268
recursive=True,
255-
include_container_content=True
269+
include_container_content=True,
270+
target_entity_types=["file"]
256271
)
257272
```
258273
"""

tests/integration/synapseclient/models/async/test_permissions_async.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -736,17 +736,21 @@ async def test_delete_permissions_recursive(self, project_model: Project) -> Non
736736
await self._set_custom_permissions(file_3)
737737

738738
# WHEN I delete permissions recursively but without container content
739-
await top_level_folder.delete_permissions_async(
740-
recursive=True, include_container_content=False
741-
)
739+
with pytest.raises(
740+
ValueError,
741+
match="When recursive=True, include_container_content must also be True",
742+
) as exc_info:
743+
await top_level_folder.delete_permissions_async(
744+
recursive=True, include_container_content=False
745+
)
742746

743747
# THEN the top_level_folder permissions should be deleted
744748
await self._verify_permissions_deleted(top_level_folder)
745749

746750
# AND the folder_1 permissions should remain (because include_container_content=False)
747751
await self._verify_permissions_not_deleted(folder_1)
748752

749-
# BUT the file_3 permissions should remain (because include_container_content=False)
753+
# AND the file_3 permissions should remain (because include_container_content=False)
750754
assert await self._verify_permissions_not_deleted(file_3)
751755

752756
async def test_delete_permissions_recursive_with_container_content(

tests/integration/synapseclient/models/synchronous/test_permissions.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -727,18 +727,22 @@ async def test_delete_permissions_recursive(self, project_model: Project) -> Non
727727
await self._set_custom_permissions(folder_1)
728728
await self._set_custom_permissions(file_3)
729729

730-
# WHEN I delete permissions recursively but without container content
731-
top_level_folder.delete_permissions(
732-
recursive=True, include_container_content=False
733-
)
730+
with pytest.raises(
731+
ValueError,
732+
match="When recursive=True, include_container_content must also be True",
733+
) as exc_info:
734+
# WHEN I delete permissions recursively but without container content
735+
top_level_folder.delete_permissions(
736+
recursive=True, include_container_content=False
737+
)
734738

735739
# THEN the top_level_folder permissions should be deleted
736740
await self._verify_permissions_deleted(top_level_folder)
737741

738742
# AND the folder_1 permissions should remain (because include_container_content=False)
739743
await self._verify_permissions_not_deleted(folder_1)
740744

741-
# BUT the file_3 permissions should remain (because include_container_content=False)
745+
# AND the file_3 permissions should remain (because include_container_content=False)
742746
assert await self._verify_permissions_not_deleted(file_3)
743747

744748
async def test_delete_permissions_recursive_with_container_content(

tests/unit/synapseclient/models/async/unit_test_permissions_async.py

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -165,24 +165,6 @@ async def test_delete_permissions_folder_recursive(self):
165165
entity_id="syn123", synapse_client=self.synapse_client
166166
)
167167

168-
async def test_sync_failure_doesnt_process_children(self):
169-
"""Test that sync failure prevents processing children."""
170-
# GIVEN a folder with an ID and mocked sync_from_synapse_async method that fails
171-
folder = Folder(id="syn123")
172-
folder.sync_from_synapse_async = AsyncMock(side_effect=Exception("Sync failed"))
173-
174-
# WHEN deleting permissions recursively
175-
await folder.delete_permissions_async(recursive=True)
176-
177-
# THEN sync_from_synapse_async should be called
178-
folder.sync_from_synapse_async.assert_called_once()
179-
180-
# AND delete_entity_acl should be called on the folder itself
181-
self.mock_delete_acl.assert_called_once()
182-
183-
# AND a warning log message should be generated
184-
self.synapse_client.logger.warning.assert_called_once()
185-
186168
async def test_filter_by_entity_type_folder(self):
187169
"""Test filtering deletion by folder entity type."""
188170
# GIVEN a folder with an ID and child folder and file
@@ -299,3 +281,23 @@ async def test_general_exception_during_delete(self):
299281

300282
# AND a warning log message should be generated
301283
self.synapse_client.logger.warning.assert_called_once()
284+
285+
async def test_invalid_recursive_without_include_container_content(self):
286+
"""Test that setting recursive=True without include_container_content=True raises ValueError."""
287+
# GIVEN a folder with an ID
288+
folder = Folder(id="syn123")
289+
290+
# WHEN attempting to delete permissions with recursive=True but include_container_content=False
291+
# THEN a ValueError should be raised
292+
with pytest.raises(
293+
ValueError,
294+
match="When recursive=True, include_container_content must also be True",
295+
):
296+
await folder.delete_permissions_async(
297+
recursive=True, include_container_content=False
298+
)
299+
300+
# AND the delete_entity_acl function should still be called for the folder itself
301+
self.mock_delete_acl.assert_called_once_with(
302+
entity_id="syn123", synapse_client=self.synapse_client
303+
)

tests/unit/synapseclient/models/unit_test_permissions.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -163,24 +163,6 @@ async def test_delete_permissions_folder_recursive(self):
163163
entity_id="syn123", synapse_client=self.synapse_client
164164
)
165165

166-
async def test_sync_failure_doesnt_process_children(self):
167-
"""Test that sync failure prevents processing children."""
168-
# GIVEN a folder with an ID and mocked sync_from_synapse_async method that fails
169-
folder = Folder(id="syn123")
170-
folder.sync_from_synapse_async = AsyncMock(side_effect=Exception("Sync failed"))
171-
172-
# WHEN deleting permissions recursively
173-
folder.delete_permissions(recursive=True)
174-
175-
# THEN sync_from_synapse_async should be called
176-
folder.sync_from_synapse_async.assert_called_once()
177-
178-
# AND delete_entity_acl should be called on the folder itself
179-
self.mock_delete_acl.assert_called_once()
180-
181-
# AND a warning log message should be generated
182-
self.synapse_client.logger.warning.assert_called_once()
183-
184166
async def test_filter_by_entity_type_folder(self):
185167
"""Test filtering deletion by folder entity type."""
186168
# GIVEN a folder with an ID and child folder and file
@@ -297,3 +279,21 @@ async def test_general_exception_during_delete(self):
297279

298280
# AND a warning log message should be generated
299281
self.synapse_client.logger.warning.assert_called_once()
282+
283+
async def test_invalid_recursive_without_include_container_content(self):
284+
"""Test that setting recursive=True without include_container_content=True raises ValueError."""
285+
# GIVEN a folder with an ID
286+
folder = Folder(id="syn123")
287+
288+
# WHEN attempting to delete permissions with recursive=True but include_container_content=False
289+
# THEN a ValueError should be raised
290+
with pytest.raises(
291+
ValueError,
292+
match="When recursive=True, include_container_content must also be True",
293+
):
294+
folder.delete_permissions(recursive=True, include_container_content=False)
295+
296+
# AND the delete_entity_acl function should still be called for the folder itself
297+
self.mock_delete_acl.assert_called_once_with(
298+
entity_id="syn123", synapse_client=self.synapse_client
299+
)

0 commit comments

Comments
 (0)