From 6c036b5b5258a24a5d653250d6d425f5a73171b0 Mon Sep 17 00:00:00 2001 From: Gonzalo Verussa Date: Thu, 29 Apr 2021 14:14:39 -0300 Subject: [PATCH 01/14] Update .gitignore with vscode and venv folder --- .gitignore | 158 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 137 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 94e70a7..b89c115 100644 --- a/.gitignore +++ b/.gitignore @@ -1,29 +1,55 @@ -# Ignore test-results -test-results.xml +# Created by https://www.toptal.com/developers/gitignore/api/python,linux,visualstudiocode +# Edit at https://www.toptal.com/developers/gitignore?templates=python,linux,visualstudiocode + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Python ### # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] +*$py.class # C extensions *.so # Distribution / packaging .Python -env/ build/ develop-eggs/ dist/ +downloads/ eggs/ .eggs/ -lib/ -lib64/ parts/ sdist/ var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ *.egg-info/ .installed.cfg *.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec # Installer logs pip-log.txt @@ -32,35 +58,125 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ +.nox/ .coverage +.coverage.* .cache nosetests.xml coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +pytestdebug.log # Translations *.mo - -# Mr Developer -.mr.developer.cfg -.project -.pydevproject - -# Rope -.ropeproject +*.pot # Django stuff: *.log -*.pot +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy # Sphinx documentation docs/_build/ +doc/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +#poetry.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +# .env +.env/ +.venv/ +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +pythonenv* + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# operating system-related files +# file properties cache/storage on macOS +*.DS_Store +# thumbnail cache on Windows +Thumbs.db + +# profiling data +.prof + + +### VisualStudioCode ### +.vscode/* +!.vscode/tasks.json +!.vscode/launch.json +*.code-workspace + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide -# IDEs -.idea -.sublime -.netbeans -*.swa -*.swp -*.swo +# End of https://www.toptal.com/developers/gitignore/api/python,linux,visualstudiocode AUTHORS From 6a167693856a9d9df8059982ed2019a1d168af53 Mon Sep 17 00:00:00 2001 From: Gonzalo Verussa Date: Thu, 29 Apr 2021 14:17:24 -0300 Subject: [PATCH 02/14] Fix package version to avoid infinite recursion and other errors in newer versions --- requirements.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index cb7c20d..43d05b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -mongomock -pyyaml -pytest-asyncio -pytest>=2.5.2 +mongomock==3.12.0 +pyyaml==3.13 +pytest-asyncio==0.10.0 +pytest==3.6.4 From 872feeaa2392c69b186f69bad7a326cc91d25e47 Mon Sep 17 00:00:00 2001 From: Gonzalo Verussa Date: Thu, 29 Apr 2021 14:27:42 -0300 Subject: [PATCH 03/14] Add MIT License --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dfa28a8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Gonzalo Verussa + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 5f8b3a212d0d08fada0656e2bbefd0c70b512f92 Mon Sep 17 00:00:00 2001 From: Gonzalo Verussa Date: Fri, 30 Apr 2021 16:20:32 -0300 Subject: [PATCH 04/14] Add FullLoader for yaml loader to support pyyaml>=5.1 --- pytest_async_mongodb/plugin.py | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pytest_async_mongodb/plugin.py b/pytest_async_mongodb/plugin.py index 329a029..bf4069e 100644 --- a/pytest_async_mongodb/plugin.py +++ b/pytest_async_mongodb/plugin.py @@ -172,7 +172,7 @@ async def load_fixture(db, collection, path, file_format): if file_format == 'json': loader = functools.partial(json.load, object_hook=json_util.object_hook) elif file_format == 'yaml': - loader = yaml.load + loader = functools.partial(yaml.load, Loader=yaml.FullLoader) else: return try: diff --git a/requirements.txt b/requirements.txt index 43d05b7..c114c98 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ mongomock==3.12.0 -pyyaml==3.13 +pyyaml>=5.1 pytest-asyncio==0.10.0 pytest==3.6.4 From 8d109f82f5ffadd738c60d3846364bdf9b20c511 Mon Sep 17 00:00:00 2001 From: Gonzalo Verussa Date: Fri, 30 Apr 2021 16:28:34 -0300 Subject: [PATCH 05/14] Skip changelog and AUTHORS file generation --- setup.cfg | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 757aca4..6f90a62 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,15 +19,16 @@ classifier = Topic :: Database Topic :: Software Development :: Libraries - [files] packages = pytest_async_mongodb - [entry_points] pytest11 = pytest-async-mongodb = pytest_async_mongodb.plugin - [wheel] universal = 1 + +[pbr] +skip_changelog = true +skip_authors = true \ No newline at end of file From 4e89889e4b115e13c5543f43410d87bf113b9071 Mon Sep 17 00:00:00 2001 From: Gonzalo Verussa Date: Mon, 3 May 2021 09:47:56 -0300 Subject: [PATCH 06/14] Change collection_names to list_collection_names to update mongomock to 3.14.0 --- pytest_async_mongodb/plugin.py | 4 ++-- requirements.txt | 2 +- tests/unit/test_plugin.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pytest_async_mongodb/plugin.py b/pytest_async_mongodb/plugin.py index bf4069e..a87360d 100644 --- a/pytest_async_mongodb/plugin.py +++ b/pytest_async_mongodb/plugin.py @@ -95,7 +95,7 @@ async def find_one(self, filter=None, *args, **kwargs): class AsyncDatabase(AsyncClassMethod, mongomock.Database): ASYNC_METHODS = [ - 'collection_names' + 'list_collection_names' ] def get_collection(self, name, codec_options=None, read_preference=None, @@ -147,7 +147,7 @@ async def async_mongodb_client(pytestconfig): async def clean_database(db): - collections = await db.collection_names(include_system_collections=False) + collections = await db.list_collection_names() for name in collections: db.drop_collection(name) diff --git a/requirements.txt b/requirements.txt index c114c98..9b20aa3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -mongomock==3.12.0 +mongomock==3.14.0 pyyaml>=5.1 pytest-asyncio==0.10.0 pytest==3.6.4 diff --git a/tests/unit/test_plugin.py b/tests/unit/test_plugin.py index b0a5e13..a4378eb 100644 --- a/tests/unit/test_plugin.py +++ b/tests/unit/test_plugin.py @@ -4,7 +4,7 @@ @pytest.mark.asyncio async def test_load(async_mongodb): - collection_names = await async_mongodb.collection_names() + collection_names = await async_mongodb.list_collection_names() assert 'players' in collection_names assert 'championships' in collection_names assert len(plugin._cache.keys()) == 2 From 7ef9a1a6473415ca48b3bafb94121e7e708b70aa Mon Sep 17 00:00:00 2001 From: Gonzalo Verussa Date: Mon, 3 May 2021 16:04:29 -0300 Subject: [PATCH 07/14] Update get_collection and get_database to use super() insted of recode it to support mongomock>=3.22.1 --- pytest_async_mongodb/plugin.py | 16 ++++++---------- requirements.txt | 2 +- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/pytest_async_mongodb/plugin.py b/pytest_async_mongodb/plugin.py index a87360d..68402ce 100644 --- a/pytest_async_mongodb/plugin.py +++ b/pytest_async_mongodb/plugin.py @@ -98,11 +98,9 @@ class AsyncDatabase(AsyncClassMethod, mongomock.Database): 'list_collection_names' ] - def get_collection(self, name, codec_options=None, read_preference=None, - write_concern=None): - collection = self._collections.get(name) - if collection is None: - collection = self._collections[name] = AsyncCollection(self, name) + def get_collection(self, *args, **kwargs) -> AsyncCollection: + collection = super().get_collection(*args, **kwargs) + collection.__class__ = AsyncCollection return collection @@ -116,11 +114,9 @@ async def __aexit__(self, exc_type, exc, tb): class AsyncMockMongoClient(mongomock.MongoClient): - def get_database(self, name, codec_options=None, read_preference=None, - write_concern=None): - db = self._databases.get(name) - if db is None: - db = self._databases[name] = AsyncDatabase(self, name) + def get_database(self, *args, **kwargs) -> AsyncDatabase: + db = super().get_database(*args, **kwargs) + db.__class__ = AsyncDatabase return db async def start_session(self, **kwargs): diff --git a/requirements.txt b/requirements.txt index 9b20aa3..d088b5a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -mongomock==3.14.0 +mongomock>=3.22.1 pyyaml>=5.1 pytest-asyncio==0.10.0 pytest==3.6.4 From f8b81705632cee7e5459943c0991ea268b263b19 Mon Sep 17 00:00:00 2001 From: Gonzalo Verussa Date: Thu, 6 May 2021 09:31:09 -0300 Subject: [PATCH 08/14] Update find_one to use super() insted of recode it and remove find of ASYNC_METHODS because its not async on async mongodb clients --- pytest_async_mongodb/plugin.py | 15 ++------------- tests/unit/test_plugin.py | 9 +++------ 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/pytest_async_mongodb/plugin.py b/pytest_async_mongodb/plugin.py index 68402ce..b49a4c3 100644 --- a/pytest_async_mongodb/plugin.py +++ b/pytest_async_mongodb/plugin.py @@ -53,7 +53,6 @@ def __getattribute__(self, name): class AsyncCollection(AsyncClassMethod, mongomock.Collection): ASYNC_METHODS = [ - 'find', 'find_one', 'find_one_and_delete', 'find_one_and_replace', @@ -76,22 +75,12 @@ class AsyncCollection(AsyncClassMethod, mongomock.Collection): 'map_reduce', ] - async def find_one(self, filter=None, *args, **kwargs): - import collections - # Allow calling find_one with a non-dict argument that gets used as - # the id for the query. - if filter is None: - filter = {} - if not isinstance(filter, collections.Mapping): - filter = {'_id': filter} - - cursor = await self.find(filter, *args, **kwargs) + async def find_one(self, *args, **kwargs): try: - return next(cursor) + return super().find_one(*args, **kwargs) except StopIteration: return None - class AsyncDatabase(AsyncClassMethod, mongomock.Database): ASYNC_METHODS = [ diff --git a/tests/unit/test_plugin.py b/tests/unit/test_plugin.py index a4378eb..bbca7ff 100644 --- a/tests/unit/test_plugin.py +++ b/tests/unit/test_plugin.py @@ -1,8 +1,9 @@ import pytest from pytest_async_mongodb import plugin +pytestmark = pytest.mark.asyncio + -@pytest.mark.asyncio async def test_load(async_mongodb): collection_names = await async_mongodb.list_collection_names() assert 'players' in collection_names @@ -12,7 +13,6 @@ async def test_load(async_mongodb): await check_championships(async_mongodb.championships) -@pytest.mark.asyncio async def check_players(players): count = await players.count_documents({}) assert count == 2 @@ -22,22 +22,19 @@ async def check_players(players): assert manuel['position'] == 'keeper' -@pytest.mark.asyncio async def check_championships(championships): count = await championships.count_documents({}) assert count == 3 await check_keys_in_docs(championships, ['year', 'host', 'winner']) -@pytest.mark.asyncio async def check_keys_in_docs(collection, keys): - docs = await collection.find() + docs = collection.find() for doc in docs: for key in keys: assert key in doc -@pytest.mark.asyncio async def test_insert(async_mongodb): await async_mongodb.players.insert_one({ 'name': 'Bastian', From a4a68c4c739b0aac057fe791200b252ae71783c0 Mon Sep 17 00:00:00 2001 From: Gonzalo Verussa Date: Thu, 6 May 2021 16:56:02 -0300 Subject: [PATCH 09/14] Change the way of dynamic creation of asynchronous methods to avoid Pytest warning for deprecation of @coroutine in pytest>=3.8 --- pytest_async_mongodb/plugin.py | 33 +++++++++++---------------------- requirements.txt | 4 ++-- 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/pytest_async_mongodb/plugin.py b/pytest_async_mongodb/plugin.py index b49a4c3..61f3eea 100644 --- a/pytest_async_mongodb/plugin.py +++ b/pytest_async_mongodb/plugin.py @@ -31,26 +31,20 @@ def pytest_addoption(parser): help='Try loading fixtures from this directory') -def wrapper(func): - @functools.wraps(func) +def async_decorator(func): async def wrapped(*args, **kwargs): - coro_func = asyncio.coroutine(func) - return await coro_func(*args, **kwargs) + return func(*args, **kwargs) return wrapped +def wrapp_methods(cls): + for method_name in cls.ASYNC_METHODS: + method = getattr(cls, method_name) + setattr(cls, method_name, async_decorator(method)) + return cls -class AsyncClassMethod(object): - ASYNC_METHODS = [] - - def __getattribute__(self, name): - attr = super(AsyncClassMethod, self).__getattribute__(name) - if type(attr) == types.MethodType and name in self.ASYNC_METHODS: - attr = wrapper(attr) - return attr - - -class AsyncCollection(AsyncClassMethod, mongomock.Collection): +@wrapp_methods +class AsyncCollection(mongomock.Collection): ASYNC_METHODS = [ 'find_one', @@ -75,13 +69,8 @@ class AsyncCollection(AsyncClassMethod, mongomock.Collection): 'map_reduce', ] - async def find_one(self, *args, **kwargs): - try: - return super().find_one(*args, **kwargs) - except StopIteration: - return None - -class AsyncDatabase(AsyncClassMethod, mongomock.Database): +@wrapp_methods +class AsyncDatabase(mongomock.Database): ASYNC_METHODS = [ 'list_collection_names' diff --git a/requirements.txt b/requirements.txt index d088b5a..fb28cb6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ mongomock>=3.22.1 pyyaml>=5.1 -pytest-asyncio==0.10.0 -pytest==3.6.4 +pytest-asyncio>=0.11.0 +pytest>=5.4 From 38bfea0a52f81e52a7f61c853109b04ca249b43c Mon Sep 17 00:00:00 2001 From: Gonzalo Verussa Date: Fri, 7 May 2021 10:22:43 -0300 Subject: [PATCH 10/14] Add AsyncCursor to make find work with async for --- pytest_async_mongodb/plugin.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pytest_async_mongodb/plugin.py b/pytest_async_mongodb/plugin.py index 61f3eea..e407f36 100644 --- a/pytest_async_mongodb/plugin.py +++ b/pytest_async_mongodb/plugin.py @@ -43,6 +43,18 @@ def wrapp_methods(cls): return cls +class AsyncCursor(mongomock.collection.Cursor): + + def __aiter__(self): + return self + + async def __anext__(self): + try: + return next(self) + except StopIteration: + raise StopAsyncIteration() + + @wrapp_methods class AsyncCollection(mongomock.Collection): @@ -69,6 +81,12 @@ class AsyncCollection(mongomock.Collection): 'map_reduce', ] + def find(self, *args, **kwargs) -> AsyncCursor: + cursor = super().find(*args, **kwargs) + cursor.__class__ = AsyncCursor + return cursor + + @wrapp_methods class AsyncDatabase(mongomock.Database): From 4d83bd5734ef7c519cddec6063c7fdabe3028f72 Mon Sep 17 00:00:00 2001 From: Gonzalo Verussa Date: Mon, 10 May 2021 09:03:31 -0300 Subject: [PATCH 11/14] Add new tests for find method --- tests/unit/fixtures/championships.json | 7 ++ tests/unit/test_plugin.py | 126 ++++++++++++++++++++++++- 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/tests/unit/fixtures/championships.json b/tests/unit/fixtures/championships.json index 695761a..1ad17b9 100644 --- a/tests/unit/fixtures/championships.json +++ b/tests/unit/fixtures/championships.json @@ -1,4 +1,11 @@ [ + + { + "_id": {"$oid": "608b0151a20cf0c679939f59"}, + "year": 2018, + "host": "Russia", + "winner": "France" + }, { "_id": {"$oid": "55d2db06f4811f83a1f27be8"}, "year": 2014, diff --git a/tests/unit/test_plugin.py b/tests/unit/test_plugin.py index bbca7ff..ebaa8b7 100644 --- a/tests/unit/test_plugin.py +++ b/tests/unit/test_plugin.py @@ -1,5 +1,6 @@ import pytest from pytest_async_mongodb import plugin +from bson import ObjectId pytestmark = pytest.mark.asyncio @@ -24,7 +25,7 @@ async def check_players(players): async def check_championships(championships): count = await championships.count_documents({}) - assert count == 3 + assert count == 4 await check_keys_in_docs(championships, ['year', 'host', 'winner']) @@ -36,12 +37,131 @@ async def check_keys_in_docs(collection, keys): async def test_insert(async_mongodb): + count_before = await async_mongodb.players.count_documents({}) await async_mongodb.players.insert_one({ 'name': 'Bastian', 'surname': 'Schweinsteiger', 'position': 'midfield' }) - count = await async_mongodb.players.count_documents({}) + count_after = await async_mongodb.players.count_documents({}) bastian = await async_mongodb.players.find_one({'name': 'Bastian'}) - assert count == 3 + assert count_after == count_before + 1 assert bastian.get('name') == 'Bastian' + + +async def test_find_one(async_mongodb): + doc = await async_mongodb.championships.find_one() + assert doc == { + "_id": ObjectId("608b0151a20cf0c679939f59"), + "year": 2018, + "host": "Russia", + "winner": "France" + } + + +async def test_find(async_mongodb): + docs = async_mongodb.championships.find() + docs_list = [] + async for doc in docs: + docs_list.append(doc) + assert docs_list == [ + { + "_id": ObjectId("608b0151a20cf0c679939f59"), + "year": 2018, + "host": "Russia", + "winner": "France" + }, + { + "_id": ObjectId("55d2db06f4811f83a1f27be8"), + "year": 2014, + "host": "Brazil", + "winner": "Germany" + }, + { + "_id": ObjectId("55d2db19f4811f83a1f27be9"), + "year": 2010, + "host": "South Africa", + "winner": "Spain" + }, + { + "_id": ObjectId("55d2db30f4811f83a1f27bea"), + "year": 2006, + "host": "Germany", + "winner": "France" + } + ] + + +async def test_find_with_filter(async_mongodb): + docs = async_mongodb.championships.find({"winner": "France"}) + docs_list = [] + async for doc in docs: + docs_list.append(doc) + assert docs_list == [ + { + "_id": ObjectId("608b0151a20cf0c679939f59"), + "year": 2018, + "host": "Russia", + "winner": "France" + }, + { + "_id": ObjectId("55d2db30f4811f83a1f27bea"), + "year": 2006, + "host": "Germany", + "winner": "France" + } + ] + + +async def test_find_sorted(async_mongodb): + docs = async_mongodb.championships.find(sort=[("year", 1)]) + docs_list = [] + async for doc in docs: + docs_list.append(doc) + assert docs_list == [ + { + "_id": ObjectId("55d2db30f4811f83a1f27bea"), + "year": 2006, + "host": "Germany", + "winner": "France" + }, + { + "_id": ObjectId("55d2db19f4811f83a1f27be9"), + "year": 2010, + "host": "South Africa", + "winner": "Spain" + }, + { + "_id": ObjectId("55d2db06f4811f83a1f27be8"), + "year": 2014, + "host": "Brazil", + "winner": "Germany" + }, + { + "_id": ObjectId("608b0151a20cf0c679939f59"), + "year": 2018, + "host": "Russia", + "winner": "France" + } + ] + + +async def test_find_sorted_with_filter(async_mongodb): + docs = async_mongodb.championships.find(filter={"winner": "France"}, sort=[("year", 1)]) + docs_list = [] + async for doc in docs: + docs_list.append(doc) + assert docs_list == [ + { + "_id": ObjectId("55d2db30f4811f83a1f27bea"), + "year": 2006, + "host": "Germany", + "winner": "France" + }, + { + "_id": ObjectId("608b0151a20cf0c679939f59"), + "year": 2018, + "host": "Russia", + "winner": "France" + } + ] \ No newline at end of file From bb1600ebe8f1d9ff7b2882f89fa748ed247dc5d4 Mon Sep 17 00:00:00 2001 From: Gonzalo Verussa Date: Mon, 10 May 2021 14:04:01 -0300 Subject: [PATCH 12/14] Format code with black --- pytest_async_mongodb/plugin.py | 95 ++++++++++++++++------------------ tests/unit/test_plugin.py | 74 +++++++++++++------------- 2 files changed, 83 insertions(+), 86 deletions(-) diff --git a/pytest_async_mongodb/plugin.py b/pytest_async_mongodb/plugin.py index e407f36..585b536 100644 --- a/pytest_async_mongodb/plugin.py +++ b/pytest_async_mongodb/plugin.py @@ -1,15 +1,12 @@ +from bson import json_util import asyncio import os import functools import json import codecs -import types - import mongomock import pytest import yaml -from bson import json_util - _cache = {} @@ -17,25 +14,29 @@ def pytest_addoption(parser): parser.addini( - name='async_mongodb_fixtures', - help='Load these fixtures for tests', - type='linelist') + name="async_mongodb_fixtures", + help="Load these fixtures for tests", + type="linelist", + ) parser.addini( - name='async_mongodb_fixture_dir', - help='Try loading fixtures from this directory', - default=os.getcwd()) + name="async_mongodb_fixture_dir", + help="Try loading fixtures from this directory", + default=os.getcwd(), + ) parser.addoption( - '--async_mongodb-fixture-dir', - help='Try loading fixtures from this directory') + "--async_mongodb-fixture-dir", help="Try loading fixtures from this directory" + ) def async_decorator(func): async def wrapped(*args, **kwargs): return func(*args, **kwargs) + return wrapped + def wrapp_methods(cls): for method_name in cls.ASYNC_METHODS: method = getattr(cls, method_name) @@ -44,7 +45,6 @@ def wrapp_methods(cls): class AsyncCursor(mongomock.collection.Cursor): - def __aiter__(self): return self @@ -59,26 +59,26 @@ async def __anext__(self): class AsyncCollection(mongomock.Collection): ASYNC_METHODS = [ - 'find_one', - 'find_one_and_delete', - 'find_one_and_replace', - 'find_one_and_update', - 'find_and_modify', - 'save', - 'delete_one', - 'delete_many', - 'count', - 'insert_one', - 'insert_many', - 'update_one', - 'update_many', - 'replace_one', - 'count_documents', - 'estimated_document_count', - 'drop', - 'create_index', - 'ensure_index', - 'map_reduce', + "find_one", + "find_one_and_delete", + "find_one_and_replace", + "find_one_and_update", + "find_and_modify", + "save", + "delete_one", + "delete_many", + "count", + "insert_one", + "insert_many", + "update_one", + "update_many", + "replace_one", + "count_documents", + "estimated_document_count", + "drop", + "create_index", + "ensure_index", + "map_reduce", ] def find(self, *args, **kwargs) -> AsyncCursor: @@ -90,9 +90,7 @@ def find(self, *args, **kwargs) -> AsyncCursor: @wrapp_methods class AsyncDatabase(mongomock.Database): - ASYNC_METHODS = [ - 'list_collection_names' - ] + ASYNC_METHODS = ["list_collection_names"] def get_collection(self, *args, **kwargs) -> AsyncCollection: collection = super().get_collection(*args, **kwargs) @@ -109,7 +107,6 @@ async def __aexit__(self, exc_type, exc, tb): class AsyncMockMongoClient(mongomock.MongoClient): - def get_database(self, *args, **kwargs) -> AsyncDatabase: db = super().get_database(*args, **kwargs) db.__class__ = AsyncDatabase @@ -120,19 +117,19 @@ async def start_session(self, **kwargs): return Session() -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") async def async_mongodb(pytestconfig): client = AsyncMockMongoClient() - db = client['pytest'] + db = client["pytest"] await clean_database(db) await load_fixtures(db, pytestconfig) return db -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") async def async_mongodb_client(pytestconfig): client = AsyncMockMongoClient() - db = client['pytest'] + db = client["pytest"] await clean_database(db) await load_fixtures(db, pytestconfig) return client @@ -145,15 +142,15 @@ async def clean_database(db): async def load_fixtures(db, config): - option_dir = config.getoption('async_mongodb_fixture_dir') - ini_dir = config.getini('async_mongodb_fixture_dir') - fixtures = config.getini('async_mongodb_fixtures') + option_dir = config.getoption("async_mongodb_fixture_dir") + ini_dir = config.getini("async_mongodb_fixture_dir") + fixtures = config.getini("async_mongodb_fixtures") basedir = option_dir or ini_dir for file_name in os.listdir(basedir): collection, ext = os.path.splitext(os.path.basename(file_name)) - file_format = ext.strip('.') - supported = file_format in ('json', 'yaml') + file_format = ext.strip(".") + supported = file_format in ("json", "yaml") selected = fixtures and collection in fixtures if selected and supported: path = os.path.join(basedir, file_name) @@ -161,16 +158,16 @@ async def load_fixtures(db, config): async def load_fixture(db, collection, path, file_format): - if file_format == 'json': + if file_format == "json": loader = functools.partial(json.load, object_hook=json_util.object_hook) - elif file_format == 'yaml': + elif file_format == "yaml": loader = functools.partial(yaml.load, Loader=yaml.FullLoader) else: return try: docs = _cache[path] except KeyError: - with codecs.open(path, encoding='utf-8') as fp: + with codecs.open(path, encoding="utf-8") as fp: _cache[path] = docs = loader(fp) for document in docs: diff --git a/tests/unit/test_plugin.py b/tests/unit/test_plugin.py index ebaa8b7..7e4613d 100644 --- a/tests/unit/test_plugin.py +++ b/tests/unit/test_plugin.py @@ -1,14 +1,14 @@ -import pytest -from pytest_async_mongodb import plugin from bson import ObjectId +from pytest_async_mongodb import plugin +import pytest pytestmark = pytest.mark.asyncio async def test_load(async_mongodb): collection_names = await async_mongodb.list_collection_names() - assert 'players' in collection_names - assert 'championships' in collection_names + assert "players" in collection_names + assert "championships" in collection_names assert len(plugin._cache.keys()) == 2 await check_players(async_mongodb.players) await check_championships(async_mongodb.championships) @@ -17,16 +17,16 @@ async def test_load(async_mongodb): async def check_players(players): count = await players.count_documents({}) assert count == 2 - await check_keys_in_docs(players, ['name', 'surname', 'position']) - manuel = await players.find_one({'name': 'Manuel'}) - assert manuel['surname'] == 'Neuer' - assert manuel['position'] == 'keeper' + await check_keys_in_docs(players, ["name", "surname", "position"]) + manuel = await players.find_one({"name": "Manuel"}) + assert manuel["surname"] == "Neuer" + assert manuel["position"] == "keeper" async def check_championships(championships): count = await championships.count_documents({}) assert count == 4 - await check_keys_in_docs(championships, ['year', 'host', 'winner']) + await check_keys_in_docs(championships, ["year", "host", "winner"]) async def check_keys_in_docs(collection, keys): @@ -38,15 +38,13 @@ async def check_keys_in_docs(collection, keys): async def test_insert(async_mongodb): count_before = await async_mongodb.players.count_documents({}) - await async_mongodb.players.insert_one({ - 'name': 'Bastian', - 'surname': 'Schweinsteiger', - 'position': 'midfield' - }) + await async_mongodb.players.insert_one( + {"name": "Bastian", "surname": "Schweinsteiger", "position": "midfield"} + ) count_after = await async_mongodb.players.count_documents({}) - bastian = await async_mongodb.players.find_one({'name': 'Bastian'}) - assert count_after == count_before + 1 - assert bastian.get('name') == 'Bastian' + bastian = await async_mongodb.players.find_one({"name": "Bastian"}) + assert count_after == count_before + 1 + assert bastian.get("name") == "Bastian" async def test_find_one(async_mongodb): @@ -55,7 +53,7 @@ async def test_find_one(async_mongodb): "_id": ObjectId("608b0151a20cf0c679939f59"), "year": 2018, "host": "Russia", - "winner": "France" + "winner": "France", } @@ -69,28 +67,28 @@ async def test_find(async_mongodb): "_id": ObjectId("608b0151a20cf0c679939f59"), "year": 2018, "host": "Russia", - "winner": "France" + "winner": "France", }, { "_id": ObjectId("55d2db06f4811f83a1f27be8"), "year": 2014, "host": "Brazil", - "winner": "Germany" + "winner": "Germany", }, { "_id": ObjectId("55d2db19f4811f83a1f27be9"), "year": 2010, "host": "South Africa", - "winner": "Spain" + "winner": "Spain", }, { "_id": ObjectId("55d2db30f4811f83a1f27bea"), "year": 2006, "host": "Germany", - "winner": "France" - } + "winner": "France", + }, ] - + async def test_find_with_filter(async_mongodb): docs = async_mongodb.championships.find({"winner": "France"}) @@ -102,14 +100,14 @@ async def test_find_with_filter(async_mongodb): "_id": ObjectId("608b0151a20cf0c679939f59"), "year": 2018, "host": "Russia", - "winner": "France" + "winner": "France", }, { "_id": ObjectId("55d2db30f4811f83a1f27bea"), "year": 2006, "host": "Germany", - "winner": "France" - } + "winner": "France", + }, ] @@ -123,31 +121,33 @@ async def test_find_sorted(async_mongodb): "_id": ObjectId("55d2db30f4811f83a1f27bea"), "year": 2006, "host": "Germany", - "winner": "France" + "winner": "France", }, { "_id": ObjectId("55d2db19f4811f83a1f27be9"), "year": 2010, "host": "South Africa", - "winner": "Spain" + "winner": "Spain", }, { "_id": ObjectId("55d2db06f4811f83a1f27be8"), "year": 2014, "host": "Brazil", - "winner": "Germany" + "winner": "Germany", }, { "_id": ObjectId("608b0151a20cf0c679939f59"), "year": 2018, "host": "Russia", - "winner": "France" - } + "winner": "France", + }, ] async def test_find_sorted_with_filter(async_mongodb): - docs = async_mongodb.championships.find(filter={"winner": "France"}, sort=[("year", 1)]) + docs = async_mongodb.championships.find( + filter={"winner": "France"}, sort=[("year", 1)] + ) docs_list = [] async for doc in docs: docs_list.append(doc) @@ -156,12 +156,12 @@ async def test_find_sorted_with_filter(async_mongodb): "_id": ObjectId("55d2db30f4811f83a1f27bea"), "year": 2006, "host": "Germany", - "winner": "France" + "winner": "France", }, { "_id": ObjectId("608b0151a20cf0c679939f59"), "year": 2018, "host": "Russia", - "winner": "France" - } - ] \ No newline at end of file + "winner": "France", + }, + ] From 82f4c14062a41791016cb731f733c032599e739d Mon Sep 17 00:00:00 2001 From: Gonzalo Verussa Date: Tue, 11 May 2021 13:22:10 -0300 Subject: [PATCH 13/14] Add pymongo to requirements to use bson --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index fb28cb6..978fc84 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ mongomock>=3.22.1 pyyaml>=5.1 pytest-asyncio>=0.11.0 pytest>=5.4 +pymongo>=3.10 \ No newline at end of file From f1f9bab078b8f05294a907549ef66cf80768650b Mon Sep 17 00:00:00 2001 From: Feng-Yin Date: Fri, 19 Aug 2022 07:21:46 +1000 Subject: [PATCH 14/14] General improvements (#5) * Support async cursor to_list * Support bulk write and aggregate * Support mongomock 4.1.2 * Add test case test_estimated_document_count * Add test case test_find_one_and_update * Add test case chained ops * Cleanup the code * Update .gitignore Co-authored-by: FengYin --- .gitignore | 3 + pytest.ini | 1 + pytest_async_mongodb/plugin.py | 125 ++++++++++++++++++++------------- requirements.txt | 4 +- tests/unit/test_plugin.py | 53 ++++++++++++++ 5 files changed, 135 insertions(+), 51 deletions(-) diff --git a/.gitignore b/.gitignore index b89c115..9a87d64 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ # Created by https://www.toptal.com/developers/gitignore/api/python,linux,visualstudiocode # Edit at https://www.toptal.com/developers/gitignore?templates=python,linux,visualstudiocode +### vscode ### +.vscode + ### Linux ### *~ diff --git a/pytest.ini b/pytest.ini index 46cdef2..ecec326 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,5 @@ [pytest] +asyncio_mode = auto async_mongodb_fixture_dir = tests/unit/fixtures ; This tests specifying exactly which fixtures to load diff --git a/pytest_async_mongodb/plugin.py b/pytest_async_mongodb/plugin.py index 585b536..b7c82da 100644 --- a/pytest_async_mongodb/plugin.py +++ b/pytest_async_mongodb/plugin.py @@ -5,8 +5,8 @@ import json import codecs import mongomock -import pytest import yaml +import pytest_asyncio _cache = {} @@ -37,11 +37,13 @@ async def wrapped(*args, **kwargs): return wrapped -def wrapp_methods(cls): - for method_name in cls.ASYNC_METHODS: - method = getattr(cls, method_name) - setattr(cls, method_name, async_decorator(method)) - return cls +def async_wrap(obj): + # wrap all the public interfaces except the one has been re-defined in obj + for item in dir(obj._base_sync_obj): + if not item.startswith("_"): + member = getattr(obj._base_sync_obj, item) + if callable(member) and item not in dir(obj): + setattr(obj, item, async_decorator(member)) class AsyncCursor(mongomock.collection.Cursor): @@ -54,48 +56,64 @@ async def __anext__(self): except StopIteration: raise StopAsyncIteration() + async def to_list(self, length=None): + the_list = [] + try: + while length is None or len(the_list) < length: + the_list.append(next(self)) + finally: + return the_list + + +class AsyncCommandCursor(mongomock.command_cursor.CommandCursor): + def __aiter__(self): + return self + + async def __anext__(self): + try: + return next(self) + except StopIteration: + raise StopAsyncIteration() + + async def to_list(self, length=None): + the_list = [] + try: + while length is None or len(the_list) < length: + the_list.append(next(self)) + finally: + return the_list -@wrapp_methods -class AsyncCollection(mongomock.Collection): - - ASYNC_METHODS = [ - "find_one", - "find_one_and_delete", - "find_one_and_replace", - "find_one_and_update", - "find_and_modify", - "save", - "delete_one", - "delete_many", - "count", - "insert_one", - "insert_many", - "update_one", - "update_many", - "replace_one", - "count_documents", - "estimated_document_count", - "drop", - "create_index", - "ensure_index", - "map_reduce", - ] + +class AsyncCollection: + def __init__(self, mongomock_collection): + self._base_sync_obj = mongomock_collection + async_wrap(self) def find(self, *args, **kwargs) -> AsyncCursor: - cursor = super().find(*args, **kwargs) + cursor = self._base_sync_obj.find(*args, **kwargs) cursor.__class__ = AsyncCursor return cursor + def aggregate(self, *args, **kwargs) -> AsyncCommandCursor: + cursor = self._base_sync_obj.aggregate(*args, **kwargs) + cursor.__class__ = AsyncCommandCursor + return cursor + + +class AsyncDatabase: + def __init__(self, mongomock_db): + self._base_sync_obj = mongomock_db + async_wrap(self) -@wrapp_methods -class AsyncDatabase(mongomock.Database): + def __getattr__(self, attr): + return self[attr] - ASYNC_METHODS = ["list_collection_names"] + def __getitem__(self, db_name): + return self.get_collection(db_name) def get_collection(self, *args, **kwargs) -> AsyncCollection: - collection = super().get_collection(*args, **kwargs) - collection.__class__ = AsyncCollection - return collection + collection = self._base_sync_obj.get_collection(*args, **kwargs) + return AsyncCollection(collection) class Session: @@ -106,29 +124,38 @@ async def __aexit__(self, exc_type, exc, tb): await asyncio.sleep(0) -class AsyncMockMongoClient(mongomock.MongoClient): +class AsyncMockMongoClient: + def __init__(self, mongomock_client): + self._base_sync_obj = mongomock_client + async_wrap(self) + + def __getattr__(self, attr): + return self[attr] + + def __getitem__(self, db_name): + return self.get_database(db_name) + def get_database(self, *args, **kwargs) -> AsyncDatabase: - db = super().get_database(*args, **kwargs) - db.__class__ = AsyncDatabase - return db + db = self._base_sync_obj.get_database(*args, **kwargs) + return AsyncDatabase(db) async def start_session(self, **kwargs): await asyncio.sleep(0) return Session() -@pytest.fixture(scope="function") -async def async_mongodb(pytestconfig): - client = AsyncMockMongoClient() +@pytest_asyncio.fixture(scope="function") +async def async_mongodb(event_loop, pytestconfig): + client = AsyncMockMongoClient(mongomock.MongoClient()) db = client["pytest"] await clean_database(db) await load_fixtures(db, pytestconfig) return db -@pytest.fixture(scope="function") -async def async_mongodb_client(pytestconfig): - client = AsyncMockMongoClient() +@pytest_asyncio.fixture(scope="function") +async def async_mongodb_client(event_loop, pytestconfig): + client = AsyncMockMongoClient(mongomock.MongoClient()) db = client["pytest"] await clean_database(db) await load_fixtures(db, pytestconfig) @@ -138,7 +165,7 @@ async def async_mongodb_client(pytestconfig): async def clean_database(db): collections = await db.list_collection_names() for name in collections: - db.drop_collection(name) + await db.drop_collection(name) async def load_fixtures(db, config): diff --git a/requirements.txt b/requirements.txt index 978fc84..3ce1cbb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ mongomock>=3.22.1 -pyyaml>=5.1 +pymongo>=3.10 pytest-asyncio>=0.11.0 pytest>=5.4 -pymongo>=3.10 \ No newline at end of file +pyyaml>=5.1 diff --git a/tests/unit/test_plugin.py b/tests/unit/test_plugin.py index 7e4613d..bc59197 100644 --- a/tests/unit/test_plugin.py +++ b/tests/unit/test_plugin.py @@ -1,6 +1,7 @@ from bson import ObjectId from pytest_async_mongodb import plugin import pytest +from pymongo import InsertOne, DESCENDING pytestmark = pytest.mark.asyncio @@ -165,3 +166,55 @@ async def test_find_sorted_with_filter(async_mongodb): "winner": "France", }, ] + + +async def test_bulk_write_and_to_list(async_mongodb): + await async_mongodb.championships.bulk_write( + [ + InsertOne({"_id": 1, "a": 22}), + InsertOne({"_id": 2, "a": 22}), + InsertOne({"_id": 3, "a": 33}), + ] + ) + result = async_mongodb.championships.find({"a": 22}) + docs = await result.to_list() + assert len(docs) == 2 + assert docs[0]["a"] == 22 + assert docs[1]["a"] == 22 + + +async def test_estimated_document_count(async_mongodb): + assert await async_mongodb.championships.estimated_document_count() == 4 + + +async def test_find_one_and_update(async_mongodb): + await async_mongodb.championships.find_one_and_update( + filter={"_id": ObjectId("608b0151a20cf0c679939f59")}, + update={"$set": {"year": 2022}}, + ) + doc = await async_mongodb.championships.find_one( + {"_id": ObjectId("608b0151a20cf0c679939f59")} + ) + assert doc["year"] == 2022 + + +async def test_chained_operations(async_mongodb): + docs = ( + await async_mongodb.championships.find() + .sort("year", DESCENDING) + .skip(1) + .limit(2) + .to_list() + ) + assert len(docs) == 2 + assert docs[0]["year"] == 2014 + assert docs[1]["year"] == 2010 + docs = ( + await async_mongodb.championships.find() + .sort("year", DESCENDING) + .skip(3) + .limit(2) + .to_list() + ) + assert len(docs) == 1 + assert docs[0]["year"] == 2006