From 577455275de311a2722c246f3a04c34e04335ed7 Mon Sep 17 00:00:00 2001 From: Dhruv Shetty Date: Fri, 13 Jun 2025 18:45:13 +0000 Subject: [PATCH 1/6] returning a list so it matches firestore --- mockfirestore/query.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mockfirestore/query.py b/mockfirestore/query.py index 179ac46..88508f4 100644 --- a/mockfirestore/query.py +++ b/mockfirestore/query.py @@ -57,12 +57,12 @@ def stream(self, transaction=None) -> Iterator[DocumentSnapshot]: if self._limit: doc_snapshots = islice(doc_snapshots, self._limit) - return iter(doc_snapshots) + return iter(list(doc_snapshots)) def get(self) -> Iterator[DocumentSnapshot]: warnings.warn('Query.get is deprecated, please use Query.stream', category=DeprecationWarning) - return self.stream() + return list(self.stream()) def _add_field_filter(self, field: str, op: str, value: Any): compare = self._compare_func(op) From 0f76d69d3199d3b0ff1494a87e48e466394d029c Mon Sep 17 00:00:00 2001 From: Dhruv Shetty Date: Thu, 26 Jun 2025 12:02:19 -0400 Subject: [PATCH 2/6] Update collection.py --- mockfirestore/collection.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/mockfirestore/collection.py b/mockfirestore/collection.py index d2af224..ddf4450 100644 --- a/mockfirestore/collection.py +++ b/mockfirestore/collection.py @@ -26,9 +26,8 @@ def document(self, document_id: Optional[str] = None) -> DocumentReference: return DocumentReference(self._data, new_path, parent=self) def get(self) -> Iterable[DocumentSnapshot]: - warnings.warn('Collection.get is deprecated, please use Collection.stream', - category=DeprecationWarning) - return self.stream() + # Stream uses a generator, so we need to convert it to a list + return list(self.stream()) @property def path(self): @@ -127,9 +126,8 @@ def document(self, document_id: Optional[str] = None, path: List[str] = None) -> return ret def get(self) -> Iterable[DocumentSnapshot]: - warnings.warn('Collection.get is deprecated, please use Collection.stream', - category=DeprecationWarning) - return self.stream() + # Stream uses a generator, so we need to convert it to a list for compatibility with firestore library + return list(self.stream()) def stream(self, transaction=None) -> Iterable[DocumentSnapshot]: for path in self._path: From 3b8a509ac0eb23f5f68f68cd8b086a8c241e5075 Mon Sep 17 00:00:00 2001 From: Dhruv Shetty Date: Thu, 26 Jun 2025 12:13:44 -0400 Subject: [PATCH 3/6] update comments --- mockfirestore/collection.py | 4 ++-- mockfirestore/query.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/mockfirestore/collection.py b/mockfirestore/collection.py index ddf4450..4e6ae90 100644 --- a/mockfirestore/collection.py +++ b/mockfirestore/collection.py @@ -26,7 +26,7 @@ def document(self, document_id: Optional[str] = None) -> DocumentReference: return DocumentReference(self._data, new_path, parent=self) def get(self) -> Iterable[DocumentSnapshot]: - # Stream uses a generator, so we need to convert it to a list + # Stream uses a generator, so we need to convert it to a list for compatibility for .get() method with firestore library return list(self.stream()) @property @@ -126,7 +126,7 @@ def document(self, document_id: Optional[str] = None, path: List[str] = None) -> return ret def get(self) -> Iterable[DocumentSnapshot]: - # Stream uses a generator, so we need to convert it to a list for compatibility with firestore library + # Stream uses a generator, so we need to convert it to a list for compatibility for .get() method with firestore library return list(self.stream()) def stream(self, transaction=None) -> Iterable[DocumentSnapshot]: diff --git a/mockfirestore/query.py b/mockfirestore/query.py index 88508f4..5698bf0 100644 --- a/mockfirestore/query.py +++ b/mockfirestore/query.py @@ -60,8 +60,7 @@ def stream(self, transaction=None) -> Iterator[DocumentSnapshot]: return iter(list(doc_snapshots)) def get(self) -> Iterator[DocumentSnapshot]: - warnings.warn('Query.get is deprecated, please use Query.stream', - category=DeprecationWarning) + # Stream uses a generator, so we need to convert it to a list for compatibility for .get() method with firestore library return list(self.stream()) def _add_field_filter(self, field: str, op: str, value: Any): From 2daa23195bb7f98145be72c7c77a5525f5bbbc9a Mon Sep 17 00:00:00 2001 From: Dhruv Shetty Date: Tue, 10 Feb 2026 11:31:41 -0500 Subject: [PATCH 4/6] Deleting a field that doesn't exist should not fail --- mockfirestore/_transformations.py | 5 ++++- tests/test_document_reference.py | 33 +++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/mockfirestore/_transformations.py b/mockfirestore/_transformations.py index 3ea6193..3697419 100644 --- a/mockfirestore/_transformations.py +++ b/mockfirestore/_transformations.py @@ -68,7 +68,10 @@ def _apply_updates(document: Dict[str, Any], data: Dict[str, Any]): def _apply_deletes(document: Dict[str, Any], data: List[str]): for key in data: path = key.split(".") - delete_by_path(document, path) + try: + delete_by_path(document, path) + except KeyError: + continue def _apply_arr_deletes(document: Dict[str, Any], data: Dict[str, Any]): diff --git a/tests/test_document_reference.py b/tests/test_document_reference.py index ae25341..e6cf58f 100644 --- a/tests/test_document_reference.py +++ b/tests/test_document_reference.py @@ -317,6 +317,39 @@ def test_document_update_transformerSentinel(self): doc = fs.collection("foo").document("first").get().to_dict() self.assertEqual(doc, {}) + def test_document_update_transformerSentinelNonExistentField(self): + fs = MockFirestore() + fs._data = {'foo': { + 'first': {'spicy': 'tuna'} + }} + fs.collection('foo').document('first').update({"nonexistent": firestore.DELETE_FIELD}) + + doc = fs.collection("foo").document("first").get().to_dict() + self.assertEqual(doc, {'spicy': 'tuna'}) + + def test_document_update_transformerSentinelNonExistentNestedField(self): + fs = MockFirestore() + fs._data = {'foo': { + 'first': {'spicy': 'tuna'} + }} + fs.collection('foo').document('first').update({"stats.student123.field": firestore.DELETE_FIELD}) + + doc = fs.collection("foo").document("first").get().to_dict() + self.assertEqual(doc, {'spicy': 'tuna'}) + + def test_document_update_transformerSentinelMixedExistingAndNonExistent(self): + fs = MockFirestore() + fs._data = {'foo': { + 'first': {'spicy': 'tuna', 'remove_me': 'gone'} + }} + fs.collection('foo').document('first').update({ + "remove_me": firestore.DELETE_FIELD, + "nonexistent": firestore.DELETE_FIELD, + }) + + doc = fs.collection("foo").document("first").get().to_dict() + self.assertEqual(doc, {'spicy': 'tuna'}) + def test_document_update_transformerArrayRemoveBasic(self): fs = MockFirestore() fs._data = {"foo": {"first": {"arr": [1, 2, 3, 4]}}} From 96c18c8c4136daa931181c8459a66b212a9f5e5b Mon Sep 17 00:00:00 2001 From: Dhruv Shetty Date: Tue, 10 Feb 2026 13:38:50 -0500 Subject: [PATCH 5/6] fix issue with .set(merge=True) --- mockfirestore/_helpers.py | 20 ++++++++++++++++++++ mockfirestore/document.py | 5 +++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/mockfirestore/_helpers.py b/mockfirestore/_helpers.py index 74222db..fd1f25b 100644 --- a/mockfirestore/_helpers.py +++ b/mockfirestore/_helpers.py @@ -77,6 +77,26 @@ def nanos(self): return str(self._timestamp).split('.')[1] +def flatten_for_merge(data: Dict[str, Any], prefix: str = '') -> Dict[str, Any]: + """Flatten nested dicts into dot-notation keys for set(merge=True). + + Firestore's set(merge=True) deep-merges at every nesting level. + The mock's update() already handles dot-notation keys correctly, + so flattening first lets the existing logic work for nested dicts. + + Only plain dicts are recursed into; Firestore transform objects + (Sentinel, ArrayUnion, etc.) are treated as leaf values. + """ + result: Dict[str, Any] = {} + for key, value in data.items(): + full_key = '{}.{}'.format(prefix, key) if prefix else key + if isinstance(value, dict): + result.update(flatten_for_merge(value, prefix=full_key)) + else: + result[full_key] = value + return result + + def get_document_iterator(document: Dict[str, Any], prefix: str = '') -> Iterator[Tuple[str, Any]]: """ :returns: (dot-delimited path, value,) diff --git a/mockfirestore/document.py b/mockfirestore/document.py index 4cc10b1..96ebc4a 100644 --- a/mockfirestore/document.py +++ b/mockfirestore/document.py @@ -4,7 +4,8 @@ from typing import List, Dict, Any from mockfirestore import NotFound from mockfirestore._helpers import ( - Timestamp, Document, Store, get_by_path, set_by_path, delete_by_path + Timestamp, Document, Store, get_by_path, set_by_path, delete_by_path, + flatten_for_merge ) from mockfirestore._transformations import apply_transformations @@ -85,7 +86,7 @@ def set(self, data: Dict, merge=False): data['__name__'] = self.id if merge: try: - self.update(data) + self.update(flatten_for_merge(data)) except NotFound: self.set(data) else: From 4118995016be8e420168d0139582901ce2b3745f Mon Sep 17 00:00:00 2001 From: Dhruv Shetty Date: Tue, 10 Feb 2026 14:00:08 -0500 Subject: [PATCH 6/6] dont' drop empty dicts --- mockfirestore/_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mockfirestore/_helpers.py b/mockfirestore/_helpers.py index fd1f25b..59637e1 100644 --- a/mockfirestore/_helpers.py +++ b/mockfirestore/_helpers.py @@ -90,7 +90,7 @@ def flatten_for_merge(data: Dict[str, Any], prefix: str = '') -> Dict[str, Any]: result: Dict[str, Any] = {} for key, value in data.items(): full_key = '{}.{}'.format(prefix, key) if prefix else key - if isinstance(value, dict): + if isinstance(value, dict) and value: result.update(flatten_for_merge(value, prefix=full_key)) else: result[full_key] = value