From faea578172632ac7e7ae1e0037aa62347ccb3f48 Mon Sep 17 00:00:00 2001 From: mrjarnould Date: Tue, 31 Mar 2026 17:19:41 +0200 Subject: [PATCH 1/2] fix: repair Notes folder desired keys --- pyicloud/services/notes/service.py | 15 ++++---- tests/test_notes.py | 58 ++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/pyicloud/services/notes/service.py b/pyicloud/services/notes/service.py index 213e3275..354a626c 100644 --- a/pyicloud/services/notes/service.py +++ b/pyicloud/services/notes/service.py @@ -56,6 +56,7 @@ from .models.dto import ChangeEvent, NoteFolder LOGGER = logging.getLogger(__name__) +_HAS_SUBFOLDER_FIELD = "HasSubfolder" class NoteNotFound(NotesError): @@ -248,8 +249,7 @@ def folders(self) -> Iterable[NoteFolder]: """ desired_keys = [ NotesDesiredKey.TITLE_ENCRYPTED, - NotesDesiredKey.TITLE_MODIFICATION_DATE, - NotesDesiredKey.HAS_SUBFOLDER, + _HAS_SUBFOLDER_FIELD, ] query = CKQueryObject( recordType="SearchIndexes", @@ -277,13 +277,12 @@ def folders(self) -> Iterable[NoteFolder]: name = self._decode_encrypted( rec.fields.get_value("TitleEncrypted") ) - has_sub = bool( - getattr( - rec.fields.get_field(NotesDesiredKey.HAS_SUBFOLDER) or (), - "value", - False, - ) + has_sub_value = getattr( + rec.fields.get_field(_HAS_SUBFOLDER_FIELD) or (), + "value", + None, ) + has_sub = None if has_sub_value is None else bool(has_sub_value) yield NoteFolder( id=folder_id, name=name, has_subfolders=has_sub, count=None ) diff --git a/tests/test_notes.py b/tests/test_notes.py index b8294fa7..3b4cd327 100644 --- a/tests/test_notes.py +++ b/tests/test_notes.py @@ -306,6 +306,64 @@ def test_notes_service_attachment_lookup_prefers_canonical_record_names(self): self.assertEqual(attachments[0].id, "Attachment/CANONICAL") self.assertIs(self.service._attachment_meta_cache["ALIAS-1"], attachments[0]) + def test_notes_service_folders_uses_supported_desired_keys(self): + """Folder listing should not depend on nonexistent Notes desired-key enums.""" + + folder_record = CKRecord.model_validate( + { + "recordName": "Folder/1", + "recordType": "SearchIndexes", + "fields": { + "TitleEncrypted": { + "type": "STRING", + "value": "Work", + "isEncrypted": True, + }, + "HasSubfolder": {"type": "INT64", "value": 1}, + }, + } + ) + self.service.raw.query = MagicMock( + return_value=MagicMock(records=[folder_record], continuationMarker=None) + ) + + folders = list(self.service.folders()) + + self.assertEqual( + self.service.raw.query.call_args.kwargs["desired_keys"], + ["TitleEncrypted", "HasSubfolder"], + ) + self.assertEqual(len(folders), 1) + self.assertEqual(folders[0].id, "Folder/1") + self.assertEqual(folders[0].name, "Work") + self.assertTrue(folders[0].has_subfolders) + + def test_notes_service_folders_treats_subfolder_flag_as_optional(self): + """Folder listing should still work when Apple omits the subfolder flag.""" + + folder_record = CKRecord.model_validate( + { + "recordName": "Folder/2", + "recordType": "SearchIndexes", + "fields": { + "TitleEncrypted": { + "type": "STRING", + "value": "Personal", + "isEncrypted": True, + }, + }, + } + ) + self.service.raw.query = MagicMock( + return_value=MagicMock(records=[folder_record], continuationMarker=None) + ) + + folders = list(self.service.folders()) + + self.assertEqual(len(folders), 1) + self.assertEqual(folders[0].name, "Personal") + self.assertIsNone(folders[0].has_subfolders) + def test_write_html_rejects_filename_escape(self): out_dir = os.path.join( tempfile.gettempdir(), From 5eb9802bb2fc7f6d3dfedaf25cd242e1358df445 Mon Sep 17 00:00:00 2001 From: mrjarnould Date: Tue, 31 Mar 2026 17:45:04 +0200 Subject: [PATCH 2/2] refactor: simplify Notes subfolder extraction --- pyicloud/services/notes/service.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pyicloud/services/notes/service.py b/pyicloud/services/notes/service.py index 354a626c..44fc0007 100644 --- a/pyicloud/services/notes/service.py +++ b/pyicloud/services/notes/service.py @@ -277,11 +277,7 @@ def folders(self) -> Iterable[NoteFolder]: name = self._decode_encrypted( rec.fields.get_value("TitleEncrypted") ) - has_sub_value = getattr( - rec.fields.get_field(_HAS_SUBFOLDER_FIELD) or (), - "value", - None, - ) + has_sub_value = rec.fields.get_value(_HAS_SUBFOLDER_FIELD) has_sub = None if has_sub_value is None else bool(has_sub_value) yield NoteFolder( id=folder_id, name=name, has_subfolders=has_sub, count=None