GithubHelp home page GithubHelp logo

python-mock-firestore's People

Contributors

ahti123 avatar anna-hope avatar billcountry avatar briggleman avatar brycethornton avatar carl-chipperfield avatar christippett avatar domanchi avatar klanderson avatar krennkristof avatar kromash avatar lsantosdemoura avatar mdowds avatar rdelg avatar satwell avatar tavva avatar ugom avatar wli avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

python-mock-firestore's Issues

Add select functionality on collections

Firstly, thank you for your work on this library. I have a project that deals with firestore a lot and being able to mock it in tests is super easy with this library.

I make use of the select method on a collection, which enables me to only pull back certain fields. This is implemented in the python library and would be great if this feature could be added.

Updating fields for nested objects (dot notation) does not work

Updating using the dot notation should update the nested field value

frank_ref = db.collection("users").document("frank")
frank_ref.set(
    {
        "name": "Frank",
        "favorites": {"food": "Pizza", "color": "Blue", "subject": "Recess"},
        "age": 12,
    }
)

# Update age and favorite color
frank_ref.update({"age": 13, "favorites.color": "Red"})

doc = db.collection("users").document("frank").get().to_dict()

assert doc["age"] == 13  # ok
assert doc["favorites.color"] == "Red"  # This shouldnt pass but passes
assert doc["favorites"]["color"] == "Red"  # This fails

Reference :
https://firebase.google.com/docs/firestore/manage-data/add-data#update_fields_in_nested_objects

Deleting documents

Hello again!

I also noticed that the delete() function for DocumentReference wasn't added.

I added the following to main.py as a quick fix but would love to hear your thoughts:

def delete(self):
    get_by_path(self._data, self._path).clear()

I also tried:

def delete(self):
    self.set({})

They both seemed to have similar behavior and passed my unit tests

Collection get() returns generator instead of list

At least in python3.7 collection get method returns generator.
Mock returns list instead.

Propose change in Query class:

def get(self) -> Generator:
        return (DocumentSnapshot(doc) for doc in self._data.values())

Add support for collection_group query

Add support for the new collection_group query that was recently added.

# Example Data
parent_1 = db.collection(u'parents').document(u'molly_smith').set({
        u'name': u'Molly Smith',
        u'age': 49
})
child_1a = db.collection(u'parents').document(u'u'molly_smith').collection(u'children').document().set({
        u'name': u'Tom Smith JR',
        u'age': 17
})
parent_2 = db.collection(u'parents').document(bill_jones').set({
        u'name': u'Bill Jones',
        u'age': 56
})
child_2a = db.collection(u'parents').document(u'bill_jones').collection(u'children').document().set({
        u'name': u'Jerry Jones',
        u'age': 17
})

# Collection Group Query
children = db.collection_group(u'children').where(u'age', u'==',17')
docs = children.stream()
for doc in docs:
    print(u'{} => {}'.format(doc.id, doc.to_dict()))

Documentation of Query

GoogleCloudPlatform Example Snippet

Getting a document creates empty document which is returned in collection.stream()

I noticed the problem in a test that asserts that a specific document does not exist and then retrieves all other documents from a collection. The previously queried document is in the stream, but a .exists check yields False. Hence I'd have to filter all stream()ed documents by checking their exists state, which I don't have to do in the original Firestore.

I have identified the problem to be the handling of empty/non-existing documents in python-mock-firestore, specifically two problems:

  1. CollectionReference.document() creates an empty document if there is no existing document for the given name. This empty document is normally treated as doc.exists == False, but CollectionReference.stream() still returns it.
  2. mock-firestore treats empty documents as non-existing. This deviates from original Firestore which can hold empty documents (and treats them as existing)

The following to tests reproduce the problem (note that they both work with Google's Firestore but fail with mock-firestore):

def test_does_not_stream_nonexisting_documents(self):
    client = MockFirestore()
    assert client.collection("foo").document("bar").get().exists == False
    assert len(list(client.collection("foo").stream())) == 0 # actual value is 1

def test_can_create_empty_documents(self):
    client = MockFirestore()
    client.collection("foo").document("bar").set({})
    assert client.collection("foo").document("bar").get().exists == True

Since I don't need empty documents in my tests, I have awkardly worked around the first issue by monkeypatching CollectionReference.stream() but I'd like to contribute a real solution to the problem. However, I am unsure how to go forward from here and would like to discuss the issue.

I propose to change document and collection lookup so that they don't create empty dicts anymore, but None values. For documents, treat None as non-existing and treat empty documents as existing. Then, to allow lazy creation of documents, _helpers.set_by_path() would have to iterate over path elements and create them instead of just trusting that the parent path exists and is a dict. This makes _helpers.set_by_path() a little slower, though, but I think it warrants the tradeoff.

WDYT?

`!=` operator is not supported in queries

Firestore supports != operators now: https://firebase.google.com/docs/firestore/query-data/queries#query_operators

I get the following error (which goes away if I use the == operator instead).

doc_snapshots = [doc_snapshot for doc_snapshot in doc_snapshots
>                    if compare(doc_snapshot._get_by_field_path(field), value)]
E   TypeError: 'NoneType' object is not callable

I believe a simple condition just needs to be added here in compare_func: https://github.com/mdowds/python-mock-firestore/blob/master/mockfirestore/query.py#L122

Implement rollback in Transaction

After #13 is merged, at some point we should make Transaction.rollback actually roll the transaction back.

This could be done by making a deep copy of the MockFirestore data before calling the write ops in Transaction._commit, and restoring the DB to that copy when rolling back.

Saving documents is very slow

Every new document save requires a deepcopy of the entire dataset, since every document contains the _data field. There must be a way around this. Why is deepcopy necessary? Can a reference to the dataset be stored instead?

Generate document id before storing document

Hello!

Thanks for making this library! Saved me a ton of time for my unit testing.

I am having an issue trying to use the library to generate document ids like such:

post_id = db.collection("posts").document().id

I do this using the live Firestore client so I was hoping it'd be possible with the mock library.

DocumentSnapshot.id missing

The Google Firestore client has an id property available on DocumentSnapshot. id seems to be set regardless of whether the document exists or not.

id = client.collection('test').document('zzz').get().id

id should be 'zzz' in this example, but with mock-firestore, this raises AttributeError: 'DocumentSnapshot' object has no attribute 'id'.

Problem with transforms on empty document

I found out that writing transforms, for example increments, on an empty doc stores the actual objects in the in-memory db, e.g.

# some code writing to the db

print(db.collection("users").document("user_one").get().to_dict())


{'num_matches_joined': <google.cloud.firestore_v1.transforms.Increment object at 0x10f2e8250>, 'scores': {'number_of_scored_games': <google.cloud.firestore_v1.transforms.Increment object at 0x10f2e8b50>, 'total_sum': <google.cloud.firestore_v1.transforms.Increment object at 0x10f2e87c0>}, 'potm_count': <google.cloud.firestore_v1.transforms.Increment object at 0x10f2e8be0>, 'record': {'num_win': <google.cloud.firestore_v1.transforms.Increment object at 0x10f2e8b20>, 'num_draw': <google.cloud.firestore_v1.transforms.Increment object at 0x10f2e8700>, 'num_loss': <google.cloud.firestore_v1.transforms.Increment object at 0x10f2e8d60>}, 'last_date_scores': {'20230503143043': 2.5}}

if instead I populate the doc before

db.collection("users").document("user_one").set({"name": "a"})

# some code writing to the db

print(db.collection("users").document("user_one").get().to_dict())

{'name': 'user_one', 'num_matches_joined': 1, 'scores': {'number_of_scored_games': 1, 'total_sum': 2.5}, 'potm_count': 0, 'record': {'num_win': 1, 'num_draw': 0, 'num_loss': 0}, 'last_date_scores': {'20230503143138': 2.5}}

Support filter kw on collections.where

google-cloud-firestore = "^2.11.1"
mock-firestore = "^0.11.0"

site-packages/google/cloud/firestore_v1/base_collection.py:290: UserWarning: Detected filter using positional arguments. Prefer using the 'filter' keyword argument instead.

i.e. instead of
col.where(_DELETED_ATTR, "==", False)

it is recommended to:

_DELETED_FILTER = FieldFilter(_DELETED_ATTR, "==", False)
col = col.where(filter=_DELETED_FILTER)

current where typesig + docs:

    def where(
        self,
        field_path: Optional[str] = None,
        op_string: Optional[str] = None,
        value=None,
        *,
        filter=None
    ) -> BaseQuery:
        """Create a "where" query with this collection as parent.

        See
        :meth:`~google.cloud.firestore_v1.query.Query.where` for
        more information on this method.

        Args:
            field_path (str): A field path (``.``-delimited list of
                field names) for the field to filter on. Optional.
            op_string (str): A comparison operation in the form of a string.
                Acceptable values are ``<``, ``<=``, ``==``, ``>=``, ``>``,
                and ``in``. Optional.
            value (Any): The value to compare the field against in the filter.
                If ``value`` is :data:`None` or a NaN, then ``==`` is the only
                allowed operation.  If ``op_string`` is ``in``, ``value``
                must be a sequence of values. Optional.
            filter (class:`~google.cloud.firestore_v1.base_query.BaseFilter`): an instance of a Filter.
                Either a FieldFilter or a CompositeFilter.
        Returns:
            :class:`~google.cloud.firestore_v1.query.Query`:
            A filtered query.
        Raises:
            ValueError, if both the positional arguments (field_path, op_string, value)
                and the filter keyword argument are passed at the same time.
        """

Should be fairly simple to extend the mock where call to have same behaviour. Any objections?

firestore transformations are not supported

Issue

firestore comes with some atomic "transformations" that are used to manipulate data without retrieving the document first, checking the value, then setting it again. These transformations are listed in their documentation: https://googleapis.dev/python/firestore/latest/transforms.html

However, mockfirestore does not support this functionality.

Reproduction Steps

mock_database.collection('user').document('some-random-id').add({'likes': 0})
mock_database.collection('user').document('some-random-id').update({'likes': firestore.Increment(1)})
assert (
    mock_database.collection('user').document('some-random-id').get().to_dict() ==
    {'likes': 1}
)

Instead, I get something like:

{'likes': <google.cloud.firestore_v1.transforms.Increment object at 0x106ee5208>}

Support Collection.add

The Collection object in the real Firestore client offers the method add. This is the method we use to create new documents. Is there a particular reason that method was omitted from MockFirestore, or did the author just not get to it?

If it's the latter, I can try implementing it.

Incorrect support of merging document

I got an error, while I'm trying to set not existing document with flag merge=True. The same code works ok for real Firestore.

from google.cloud import firestore
from mockfirestore import MockFirestore


COLLECTION = 'test'

db = firestore.Client.from_service_account_json('firestore.json')
pk = 'test3'

v_not_exist = db.collection(COLLECTION).document(pk).get()
print(f'v_not_exist: {v_not_exist.exists}')

db.collection(COLLECTION).document(pk).set({'first': 1, 'v': 1}, merge=True)

v1 = db.collection(COLLECTION).document(pk).get()
print(f'v1: {v1._data}')

db.collection(COLLECTION).document(pk).set({'second': 2, 'v': 2}, merge=True)
v2 = db.collection(COLLECTION).document(pk).get()
print(f'v2: {v2._data}')

print('---')

mock_db = MockFirestore()
v_not_exist = mock_db.collection(COLLECTION).document(pk).get()
print(f'm v_not_exist: {v_not_exist.exists}')

mock_db.collection(COLLECTION).document(pk).set({'first': 1, 'v': 1}, merge=True)

v1 = mock_db.collection(COLLECTION).document(pk).get()
print(f'm v1: {v1.to_dict()}')


mock_db.collection(COLLECTION).document(pk).set({'second': 2, 'v': 2}, merge=True)
v2 = mock_db.collection(COLLECTION).document(pk).get()
print(f'm v2: {v2.to_dict()}')

Output:

v_not_exist: True
v1: {'second': 2, 'v': 1, 'first': 1}
v2: {'second': 2, 'v': 2, 'first': 1}
---
m v_not_exist: False
Traceback (most recent call last):
  File "/home/peter/projects/af/notes/personal/firestore_learn/personal.py", line 39, in <module>
    mock_db.collection(COLLECTION).document(pk).set({'first': 1, 'v': 1}, merge=True)
  File "/home/peter/.pyenv/versions/firestore_learn_py3.8.1/lib/python3.8/site-packages/mockfirestore/document.py", line 56, in set
    self.update(deepcopy(data))
  File "/home/peter/.pyenv/versions/firestore_learn_py3.8.1/lib/python3.8/site-packages/mockfirestore/document.py", line 63, in update
    raise NotFound('No document to update: {}'.format(self._path))
google.api_core.exceptions.NotFound: 404 No document to update: ['learn', 'test3']

DocumentSnapshot.get() missing

The DocumentSnapshot has a get function, which seems to be missing here. Large sections of our source code uses the get function. It would be great if this could be added.

DocumentSnapshot.get() should return None instead of throwing a KeyError

The current implementation works pretty well, but it has one flaw. It throws a KeyError when the document itself does not exist, but a subcollection does. According to the actual Firestore implementation it considers the document to be non-existent, even when a subcollection for that path exists.

Example:

users/foo
<no data>

users/foo/items
{
  "name": "My Item"
}

then

db.collection("users").document("foo").get().get("name")  # should return None, but throws KeyError instead

I briefly glared at the source code and I think it's because it stores both document and subcollection data in the same _doc property.

Query._apply_cursor is not working for similar objects

Following test will fail:

    def test_collection_start_after_similar_objects(self):
        fs = MockFirestore()
        fs._data = {'foo': {
            'first': {'id': 1, 'value': 1},
            'second': {'id': 2, 'value': 2},
            'third': {'id': 3, 'value': 2},
            'fourth': {'id': 4, 'value': 3}
        }}
        docs = list(fs.collection('foo').order_by('id').start_after({'id': 3, 'value': 2}).stream())
        self.assertEqual({'id': 4, 'value': 3}, docs[0].to_dict())
        self.assertEqual(1, len(docs))

Only last key in document_fields decides if index is set or not because of missing brake in search-compare loop inside Query._apply_cursor:

        for idx, doc in enumerate(doc_snapshot):
            index = None
            for k, v in document_fields.items():
                if doc.to_dict().get(k, None) == v:
                    index = idx
                else:
                    index = None
                    # missing break
            if index:
                ...
    

Incorrect return type for collection.get() or collection.stream()

From the line

arr = self.mock_db.collection('players').stream()

I expected a return type of Iterable[DocumentSnapshot] but instead got

  <generator object CollectionReference.stream at 0x106a1d890>

My setup code:

    def setUp(self) -> None:
            self.mock_db = MockFirestore()
            self.mock_db.collection('players').add({
                'name': 'John Doe',
                'id': 'johndoe',
            })
            self.mock_db.collection('players').add({
               'name': 'Jane Doe',
               'id': 'johndoe',
            })

Screenshot of Pycharm's documentation

Screen Shot 2021-10-24 at 10 41 44 AM

Query._apply_cursor is not working for first element

Hello,

Got problem with start_after and after some digging I found that Query._apply_cursor is not working properly when first doc is passed.

Let's take a look at problematic fragment:

        for idx, doc in enumerate(doc_snapshot):
            index = None
            for k, v in document_fields.items():
                if doc.to_dict().get(k, None) == v:
                    index = idx
                else:
                    index = None
            if index:
                if before and start:
                    return islice(docs, index, None, None)
                ...

We search for index of matching document in inner for loop but if found index equals 0 it won't go inside if index:

Following test will fail on current version:

    def test_collection_start_after(self):
        fs = MockFirestore()
        fs._data = {'foo': {
            'first': {'id': 1},
            'second': {'id': 2},
            'third': {'id': 3}
        }}
        docs = list(fs.collection('foo').start_after({'id': 1}).stream())
        self.assertEqual({'id': 2}, docs[0].to_dict())
        self.assertEqual(2, len(docs))

Solution:
In _apply_cursor add explicit check for None

if index is not None:

Add merge parameter to set()

Really appreciate your work on this library. I'm using it in a suite of unit tests, but I'm missing the merge functionality in DocumentReference.set().

If you're open to PRs, I'm happy to contribute.

Exception Should be thrown for NotFound Document

Non existent docs should raise an exception.

Example:

try:
    doc = db.collection('users').document('thisDoesNotExist').get()
    print(u'Document data: {}'.format(doc.to_dict()))
except google.cloud.exceptions.NotFound:
    print(u'No such document!')

Use black formatting

As black is becoming the de-facto standard formatter for Python code, I think we should use it in this repository. The formatting is wonky in multiple places, and using black would make the code more readable.

update the github project with the latest version

I needed async support and spent a whole day implementing it. Just before I went to submit a merge request I found the MyPy page with a newer version containing an async implementation. So sad. :(

ArrayUnion should only add elements not already present

According to the documentation ArrayUnion should only add elements that aren't already present in the array. E.g. this test should succeed:

    def test_document_update_transformerArrayUnionDuplicates(self):
        fs = MockFirestore()
        fs._data = {"foo": {"first": {"arr": [1, 3]}}}
        fs.collection("foo").document("first").update(
            {"arr": firestore.ArrayUnion([2, 3, 4])}
        )
        doc = fs.collection("foo").document("first").get().to_dict()
        self.assertCountEqual(doc["arr"], [1, 2, 3, 4])  # fails: doc["arr"] = [1, 3, 2, 3, 4]

array_contains throws exception if array in colelction is none

....\venv\Lib\site-packages\parameters_validation\validate_parameters_decorator.py:37: in wrapper
return f(*args, **kwargs)
....\svc\db\db.py:94: in get_tails_for_user
docs = tails_ref.stream()
....\venv\Lib\site-packages\mockfirestore\query.py:31: in stream
doc_snapshots = [doc_snapshot for doc_snapshot in doc_snapshots
....\venv\Lib\site-packages\mockfirestore\query.py:32: in
if compare(doc_snapshot._get_by_field_path(field), value)]


x = None, y = 'red'

return lambda x, y: y in x
E TypeError: argument of type 'NoneType' is not iterable

....\venv\Lib\site-packages\mockfirestore\query.py:137: TypeError

Different instances don't share data

In the intended use of different instances to unit test Firestore behavior, this is not available using MockFirestore:

ex:

fire_db_a = MockFirestore()
fire_db_b = MockFirestore()

doc_ref_a = fire_db.collection('test').document('abc')
doc_ref_a.set({ 'test': 'value'})
doc_ref_b = fire_db.collection('test').document('abc')
doc_ref_a.get().to_dict()
// returns {'test': 'value'}
doc_ref_b.get().to_dict()
// returns {}

returns should be the same

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.