Skip to content

Commit cab5dfd

Browse files
Support the new best practices of where(filter=FieldFilter... (#2)
1 parent 0de34b1 commit cab5dfd

File tree

11 files changed

+939
-19
lines changed

11 files changed

+939
-19
lines changed

.github/workflows/pr_agent.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
on:
2+
pull_request:
3+
types: [opened, reopened, ready_for_review]
4+
issue_comment:
5+
jobs:
6+
pr_agent_job:
7+
if: ${{ github.event.sender.type != 'Bot' }}
8+
runs-on: ubuntu-latest
9+
permissions:
10+
issues: write
11+
pull-requests: write
12+
contents: write
13+
name: Run pr agent on every pull request, respond to user comments
14+
steps:
15+
- name: PR Agent action step
16+
id: pragent
17+
uses: qodo-ai/[email protected]
18+
with:
19+
args: '/improve --pr_code_suggestions.commitable_code_suggestions=true'
20+
env:
21+
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
22+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/test.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
name: Test
2-
on: [push, pull_request]
2+
on: [ push, pull_request ]
33

44
jobs:
55
test:
66
runs-on: ubuntu-latest
77
strategy:
88
matrix:
9-
python-version: [ '3.6', '3.7', '3.8', '3.9', '3.10' ]
9+
python-version: [ '3.9', '3.10', '3.11', '3.12' ]
1010

1111
steps:
12-
- uses: actions/checkout@v2
12+
- name: Check out repository
13+
uses: actions/checkout@v4
14+
1315
- name: Set up Python ${{ matrix.python-version }}
14-
uses: actions/setup-python@v2
16+
uses: actions/setup-python@v4
1517
with:
1618
python-version: ${{ matrix.python-version }}
1719
- name: Install dependencies

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,4 @@ venv.bak/
106106
.mypy_cache/
107107

108108
.idea/
109+
.qodo

agent-coding-standards.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Agent Coding Standards
2+
3+
# ALL CODE MUSE BE UP TO THIS STANDARD
4+
5+
## Follow Clean Code principles (Robert C. Martin) when writing code
6+
7+
- Specifically the correct order of functions
8+
9+
## No comments!
10+
11+
- Most likely there is absolutely no reason to add a comment
12+
- If there is, it's probably a sign that the code is not clear
13+
14+
## Don't use dicts.
15+
16+
- str keys are red
17+
- Use pydantic models instead
18+
19+
## Do not hallucinate
20+
21+
- If you are not sure, ask the user
22+
- If you don't know the library, do the research
23+
24+
## Do not regress when moving code
25+
26+
- Make sure quality of moved code is at least as good as the original
27+
28+
## Don't make me ask you twice
29+
30+
- Follow all these rules
31+
- Every time, all the time
32+
- If it's hard, ask me how to solve it
33+
34+
## Only essential complexity, not accidental
35+
36+
- Use the simplest approach that works
37+
- Question whether each line adds value or just complexity

mockfirestore/collection.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import warnings
22
from typing import Any, List, Optional, Iterable, Dict, Tuple, Sequence, Union
33

4+
from google.cloud.firestore_v1 import FieldFilter
5+
46
from mockfirestore import AlreadyExists
57
from mockfirestore._helpers import generate_random_string, Store, get_by_path, set_by_path, Timestamp
6-
from mockfirestore.query import Query
78
from mockfirestore.document import DocumentReference, DocumentSnapshot
9+
from mockfirestore.query import Query
810

911

1012
class CollectionReference:
@@ -41,9 +43,10 @@ def add(self, document_data: Dict, document_id: str = None) \
4143
timestamp = Timestamp.from_now()
4244
return timestamp, doc_ref
4345

44-
def where(self, field: str, op: str, value: Any) -> Query:
45-
query = Query(self, field_filters=[(field, op, value)])
46-
return query
46+
def where(self, field: str = None, op: str = None, value: Any = None, *, filter: FieldFilter = None) -> Query:
47+
if filter is not None:
48+
return Query(self, field_filters=[(filter.field_path, filter.op_string, filter.value)])
49+
return Query(self, field_filters=[(field, op, value)])
4750

4851
def order_by(self, key: str, direction: Optional[str] = None) -> Query:
4952
query = Query(self, orders=[(key, direction)])
@@ -82,4 +85,4 @@ def list_documents(self, page_size: Optional[int] = None) -> Sequence[DocumentRe
8285
def stream(self, transaction=None) -> Iterable[DocumentSnapshot]:
8386
for key in sorted(get_by_path(self._data, self._path)):
8487
doc_snapshot = self.document(key).get()
85-
yield doc_snapshot
88+
yield doc_snapshot

mockfirestore/query.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import warnings
22
from itertools import islice, tee
3-
from typing import Iterator, Any, Optional, List, Callable, Union
3+
from typing import Any, Callable, Iterator, Optional, Union
4+
5+
from google.cloud.firestore_v1 import CollectionReference, FieldFilter
46

5-
from mockfirestore.document import DocumentSnapshot
67
from mockfirestore._helpers import T
8+
from mockfirestore.document import DocumentSnapshot
79

810

911
class Query:
10-
def __init__(self, parent: 'CollectionReference', projection=None,
12+
def __init__(self, parent: CollectionReference, projection=None,
1113
field_filters=(), orders=(), limit=None, offset=None,
1214
start_at=None, end_at=None, all_descendants=False) -> None:
1315
self.parent = parent
@@ -61,8 +63,18 @@ def _add_field_filter(self, field: str, op: str, value: Any):
6163
compare = self._compare_func(op)
6264
self._field_filters.append((field, compare, value))
6365

64-
def where(self, field: str, op: str, value: Any) -> 'Query':
65-
self._add_field_filter(field, op, value)
66+
def where(
67+
self,
68+
field: str = None,
69+
op: str = None,
70+
value: Any = None,
71+
*,
72+
filter: FieldFilter = None,
73+
) -> "Query":
74+
if filter is not None:
75+
self._add_field_filter(filter.field_path, filter.op_string, filter.value)
76+
else:
77+
self._add_field_filter(field, op, value)
6678
return self
6779

6880
def order_by(self, key: str, direction: Optional[str] = 'ASCENDING') -> 'Query':

pyproject.toml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
[build-system]
2+
requires = ["setuptools>=42", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "mock-firestore"
7+
version = "0.12.0"
8+
description = "A mocked version of the Google Cloud Firestore library"
9+
authors = [{ name = "Matt Dowds" }]
10+
license = { text = "MIT" }
11+
readme = "README.md"
12+
requires-python = ">=3.9"
13+
classifiers = [
14+
"Programming Language :: Python :: 3",
15+
"Programming Language :: Python :: 3.9",
16+
"Programming Language :: Python :: 3.10",
17+
"Programming Language :: Python :: 3.11",
18+
"License :: OSI Approved :: MIT License",
19+
"Operating System :: OS Independent",
20+
]
21+
dependencies = ["google-cloud-firestore"]
22+
23+
[project.optional-dependencies]
24+
dev = ["google-cloud-firestore", "pytest", "black", "isort", "mypy", "flake8"]
25+
26+
[project.urls]
27+
"Homepage" = "https://github.com/mdowds/mock-firestore"
28+
"Bug Tracker" = "https://github.com/mdowds/mock-firestore/issues"
29+
30+
[tool.black]
31+
line-length = 88
32+
target-version = ["py39"]
33+
34+
[tool.isort]
35+
profile = "black"
36+
line_length = 88
37+
38+
[tool.mypy]
39+
python_version = "3.9"
40+
warn_return_any = true
41+
warn_unused_configs = true
42+
disallow_untyped_defs = true
43+
disallow_incomplete_defs = true
44+
45+
[dependency-groups]
46+
dev = ["pytest>=8.3.5"]

requirements-dev-minimal.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
google-cloud-firestore
1+
google-cloud-firestore

setup.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setuptools.setup(
77
name="mock-firestore",
8-
version="0.11.0",
8+
version="0.12.0",
99
author="Matt Dowds",
1010
description="In-memory implementation of Google Cloud Firestore for use in tests",
1111
long_description=long_description,
@@ -14,11 +14,9 @@
1414
packages=setuptools.find_packages(),
1515
test_suite='',
1616
classifiers=[
17-
'Programming Language :: Python :: 3.6',
18-
'Programming Language :: Python :: 3.7',
19-
'Programming Language :: Python :: 3.8',
2017
'Programming Language :: Python :: 3.9',
2118
'Programming Language :: Python :: 3.10',
19+
'Programming Language :: Python :: 3.11',
2220
"License :: OSI Approved :: MIT License",
2321
],
2422
)

tests/test_where_field.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
from unittest import TestCase
2+
3+
from google.cloud.firestore_v1 import FieldFilter
4+
5+
from mockfirestore import MockFirestore
6+
7+
8+
class TestWhereField(TestCase):
9+
def test_collection_whereEquals(self):
10+
fs = MockFirestore()
11+
fs._data = {'foo': {
12+
'first': {'valid': True},
13+
'second': {'gumby': False}
14+
}}
15+
16+
docs = list(fs.collection('foo').where(field='valid', op='==', value=True).stream())
17+
self.assertEqual({'valid': True}, docs[0].to_dict())
18+
19+
def test_collection_whereEquals_with_filter(self):
20+
fs = MockFirestore()
21+
fs._data = {'foo': {
22+
'first': {'valid': True},
23+
'second': {'gumby': False}
24+
}}
25+
26+
docs = list(fs.collection('foo').where(filter=FieldFilter('valid', '==', True)).stream())
27+
self.assertEqual({'valid': True}, docs[0].to_dict())
28+
29+
def test_collection_whereNotEquals(self):
30+
fs = MockFirestore()
31+
fs._data = {'foo': {
32+
'first': {'count': 1},
33+
'second': {'count': 5}
34+
}}
35+
36+
docs = list(fs.collection('foo').where('count', '!=', 1).stream())
37+
self.assertEqual({'count': 5}, docs[0].to_dict())
38+
39+
def test_collection_whereLessThan(self):
40+
fs = MockFirestore()
41+
fs._data = {'foo': {
42+
'first': {'count': 1},
43+
'second': {'count': 5}
44+
}}
45+
46+
docs = list(fs.collection('foo').where('count', '<', 5).stream())
47+
self.assertEqual({'count': 1}, docs[0].to_dict())
48+
49+
def test_collection_whereLessThanOrEqual(self):
50+
fs = MockFirestore()
51+
fs._data = {'foo': {
52+
'first': {'count': 1},
53+
'second': {'count': 5}
54+
}}
55+
56+
docs = list(fs.collection('foo').where('count', '<=', 5).stream())
57+
self.assertEqual({'count': 1}, docs[0].to_dict())
58+
self.assertEqual({'count': 5}, docs[1].to_dict())
59+
60+
def test_collection_whereGreaterThan(self):
61+
fs = MockFirestore()
62+
fs._data = {'foo': {
63+
'first': {'count': 1},
64+
'second': {'count': 5}
65+
}}
66+
67+
docs = list(fs.collection('foo').where('count', '>', 1).stream())
68+
self.assertEqual({'count': 5}, docs[0].to_dict())
69+
70+
def test_collection_whereGreaterThanOrEqual(self):
71+
fs = MockFirestore()
72+
fs._data = {'foo': {
73+
'first': {'count': 1},
74+
'second': {'count': 5}
75+
}}
76+
77+
docs = list(fs.collection('foo').where('count', '>=', 1).stream())
78+
self.assertEqual({'count': 1}, docs[0].to_dict())
79+
self.assertEqual({'count': 5}, docs[1].to_dict())
80+
81+
def test_collection_whereMissingField(self):
82+
fs = MockFirestore()
83+
fs._data = {'foo': {
84+
'first': {'count': 1},
85+
'second': {'count': 5}
86+
}}
87+
88+
docs = list(fs.collection('foo').where('no_field', '==', 1).stream())
89+
self.assertEqual(len(docs), 0)
90+
91+
def test_collection_whereNestedField(self):
92+
fs = MockFirestore()
93+
fs._data = {'foo': {
94+
'first': {'nested': {'a': 1}},
95+
'second': {'nested': {'a': 2}}
96+
}}
97+
98+
docs = list(fs.collection('foo').where('nested.a', '==', 1).stream())
99+
self.assertEqual(len(docs), 1)
100+
self.assertEqual({'nested': {'a': 1}}, docs[0].to_dict())
101+
102+
def test_collection_whereIn(self):
103+
fs = MockFirestore()
104+
fs._data = {'foo': {
105+
'first': {'field': 'a1'},
106+
'second': {'field': 'a2'},
107+
'third': {'field': 'a3'},
108+
'fourth': {'field': 'a4'},
109+
}}
110+
111+
docs = list(fs.collection('foo').where('field', 'in', ['a1', 'a3']).stream())
112+
self.assertEqual(len(docs), 2)
113+
self.assertEqual({'field': 'a1'}, docs[0].to_dict())
114+
self.assertEqual({'field': 'a3'}, docs[1].to_dict())
115+
116+
def test_collection_whereArrayContains(self):
117+
fs = MockFirestore()
118+
fs._data = {'foo': {
119+
'first': {'field': ['val4']},
120+
'second': {'field': ['val3', 'val2']},
121+
'third': {'field': ['val3', 'val2', 'val1']}
122+
}}
123+
124+
docs = list(fs.collection('foo').where('field', 'array_contains', 'val1').stream())
125+
self.assertEqual(len(docs), 1)
126+
self.assertEqual(docs[0].to_dict(), {'field': ['val3', 'val2', 'val1']})
127+
128+
def test_collection_whereArrayContainsAny(self):
129+
fs = MockFirestore()
130+
fs._data = {'foo': {
131+
'first': {'field': ['val4']},
132+
'second': {'field': ['val3', 'val2']},
133+
'third': {'field': ['val3', 'val2', 'val1']}
134+
}}
135+
136+
contains_any_docs = list(fs.collection('foo').where('field', 'array_contains_any', ['val1', 'val4']).stream())
137+
self.assertEqual(len(contains_any_docs), 2)
138+
self.assertEqual({'field': ['val4']}, contains_any_docs[0].to_dict())
139+
self.assertEqual({'field': ['val3', 'val2', 'val1']}, contains_any_docs[1].to_dict())

0 commit comments

Comments
 (0)