Skip to content

Commit fbf3c2e

Browse files
committed
fix(sharepoint): resolve list item entity type before batched create
ListItem.ensure_type_name queued the create while _entity_type_name was still unset, so execute_batch serialized the item as SP.ListItem; a multi-lookup collection was then rejected as an open collection claim. Queue a deferred barrier after the ListItemEntityTypeFullName read so the annotation is adopted before the create round is built. Drop the unused action parameter and cover the cold and cached paths offline. Refs #717
1 parent 3e824fa commit fbf3c2e

3 files changed

Lines changed: 66 additions & 7 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
5959
`/_api/$batch` is flagged `batchable = False` and run on its own. Fixes
6060
`DocumentSet.create(...)` followed by `execute_batch()`
6161
([#868](https://github.com/vgrem/office365-rest-python-client/issues/868)).
62+
- **List item creates in batches:** `ListItem.ensure_type_name` now defers the
63+
queued create until `ListItemEntityTypeFullName` is read, so an `add_item` /
64+
`update` batched via `execute_batch` is serialized with the correct
65+
`SP.Data.<List>ListItem` annotation instead of the open `SP.ListItem` type.
66+
Fixes "An open collection property ... was found"
67+
([#717](https://github.com/vgrem/office365-rest-python-client/issues/717)).
6268

6369
### Internal
6470
- **Developer onboarding rework:** credentials flow through `tests/settings.py` with

‎office365/sharepoint/listitems/listitem.py‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -787,21 +787,23 @@ def _tax_text_field_loaded(return_type):
787787

788788
tax_field.ensure_property("TextField").after_execute(lambda _: _tax_field_loaded())
789789

790-
def ensure_type_name(self, target_list, action=None):
791-
"""Determine metadata annotation for ListItem entity
790+
def ensure_type_name(self, target_list) -> Self:
791+
"""Determine the metadata type annotation for this list item.
792+
793+
Adopts the target list's ``ListItemEntityTypeFullName`` once it is available.
794+
When the list has not resolved it yet, a deferred barrier is queued right after
795+
the read, so the annotation is set before any query built afterwards executes
796+
-- including inside ``execute_batch``.
792797
793798
Args:
794799
target_list (office365.sharepoint.lists.list.List): List resource
795-
action (() -> None): Event handler
796800
"""
797801
if self._entity_type_name is None:
802+
from office365.runtime.queries.deferred import DeferredOperationQuery
798803

799804
def _list_loaded():
800805
self._entity_type_name = target_list.properties["ListItemEntityTypeFullName"]
801-
if callable(action):
802-
action()
803806

804807
target_list.ensure_property("ListItemEntityTypeFullName").after_execute(lambda _: _list_loaded())
805-
elif callable(action):
806-
action()
808+
target_list.context.add_query(DeferredOperationQuery(target_list.context))
807809
return self

‎tests/unit/test_runtime_batch.py‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,13 +662,15 @@ def __init__(self, bodies_per_round: list[list[dict]], single_bodies: list[dict]
662662
self._bodies = bodies_per_round
663663
self._single_bodies = single_bodies or []
664664
self.rounds: list[list[str]] = []
665+
self.payloads: list[str] = []
665666
self.singles: list[str] = []
666667

667668
def execute(self, request):
668669
if "$batch" in request.url:
669670
payload = request.data
670671
if isinstance(payload, bytes):
671672
payload = payload.decode("utf-8", "replace")
673+
self.payloads.append(payload)
672674
lines = [
673675
line.strip()
674676
for line in payload.splitlines()
@@ -799,6 +801,55 @@ def test_document_set_create_resolves_deferred_barrier():
799801
assert transport.singles == ["POST https://contoso.sharepoint.com/_vti_bin/listdata.svc/SharedDocuments HTTP/1.1"]
800802

801803

804+
# ── #717: list item entity type must be resolved before the create round ──────
805+
806+
_LIST_TYPE_BODY = {
807+
"d": {
808+
"__metadata": {"type": "SP.List"},
809+
"ListItemEntityTypeFullName": "SP.Data.MyListListItem",
810+
}
811+
}
812+
813+
814+
def test_add_item_resolves_entity_type_before_create_round():
815+
"""#717: the create must not be batched with the list's type-name read.
816+
817+
The annotation (``SP.Data.<List>ListItem``) is read in one round, then the POST
818+
is serialized in a later round; otherwise the server sees a multi-lookup value
819+
collection on an ``SP.ListItem`` and rejects it as an open collection.
820+
"""
821+
transport = _ScriptedBatchTransport([[_LIST_TYPE_BODY], [{"d": {}}]])
822+
ctx = _sharepoint_ctx(transport)
823+
lst = ctx.web.lists.get_by_title("MyList")
824+
825+
item = lst.add_item({"Title": "A1"})
826+
ctx.execute_batch()
827+
828+
assert len(transport.rounds) == 2, transport.rounds # noqa: PLR2004
829+
assert transport.rounds[0][0].startswith("GET ")
830+
assert transport.rounds[1][0].startswith("POST ")
831+
assert "/items" in transport.rounds[1][0]
832+
assert item.entity_type_name == "SP.Data.MyListListItem"
833+
assert "SP.Data.MyListListItem" in transport.payloads[1]
834+
835+
836+
def test_add_item_reuses_cached_entity_type_without_reading_it_again():
837+
"""Once cached, the type annotation is adopted without an extra list read."""
838+
transport = _ScriptedBatchTransport([[{"d": {}}]])
839+
ctx = _sharepoint_ctx(transport)
840+
lst = ctx.web.lists.get_by_title("MyList")
841+
lst.set_property("Id", "11111111-2222-3333-4444-555555555555")
842+
lst.set_property("ListItemEntityTypeFullName", "SP.Data.MyListListItem")
843+
844+
item = lst.add_item({"Title": "A1"})
845+
ctx.execute_batch()
846+
847+
assert len(transport.rounds) == 1, transport.rounds
848+
assert transport.rounds[0][0].startswith("POST ")
849+
assert item.entity_type_name == "SP.Data.MyListListItem"
850+
assert "SP.Data.MyListListItem" in transport.payloads[0]
851+
852+
802853
def test_document_set_create_drains_all_rounds_async():
803854
"""Async twin of the multi-phase ``DocumentSet.create`` batch chain."""
804855
transport = _ScriptedBatchTransport([[_FOLDER_BODY], [_LIST_BODY]], single_bodies=[_CREATE_BODY])

0 commit comments

Comments
 (0)