From 0c562e940b30c5e2e20777f281cba6b1d250a48a Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 20 Apr 2022 19:47:14 -0500 Subject: [PATCH] MOTOR-902 Since Python 3.10 asyncio.get_event_loop() is deprecated (#155) --- .github/workflows/test-python.yml | 2 +- doc/examples/aiohttp_example.py | 3 +- doc/examples/aiohttp_gridfs_example.py | 2 +- doc/examples/auto_csfle_example.py | 3 +- ...encryption_automatic_decryption_example.py | 3 +- doc/examples/explicit_encryption_example.py | 3 +- .../server_fle_enforcement_example.py | 3 +- doc/tutorial-asyncio.rst | 28 +++++++------ motor/core.py | 41 ++++++++++--------- motor/frameworks/asyncio/__init__.py | 6 ++- setup.py | 12 ++++-- tox.ini | 2 + 12 files changed, 60 insertions(+), 48 deletions(-) diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index b2a10abf..441d3484 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -39,7 +39,7 @@ jobs: mongod --fork --dbpath=$(pwd)/data --logpath=$PWD/mongo.log --setParameter enableTestCommands=1 - name: Install Python dependencies run: | - python -m pip install -U pip tox tox-gh-actions + python -m pip install -U pip tox tox-gh-actions setuptools - name: Run tests run: | tox diff --git a/doc/examples/aiohttp_example.py b/doc/examples/aiohttp_example.py index 291a645c..0cd55a21 100644 --- a/doc/examples/aiohttp_example.py +++ b/doc/examples/aiohttp_example.py @@ -41,8 +41,7 @@ async def page_handler(request): # -- handler-end -- # -- main-start -- -loop = asyncio.get_event_loop() -db = loop.run_until_complete(setup_db()) +db = asyncio.run(setup_db()) app = web.Application() app["db"] = db # Route requests to the page_handler() coroutine. diff --git a/doc/examples/aiohttp_gridfs_example.py b/doc/examples/aiohttp_gridfs_example.py index d55b2c3c..b0dddc65 100644 --- a/doc/examples/aiohttp_gridfs_example.py +++ b/doc/examples/aiohttp_gridfs_example.py @@ -33,7 +33,7 @@ async def put_gridfile(): ) -asyncio.get_event_loop().run_until_complete(put_gridfile()) +asyncio.run(put_gridfile()) # Add "Content-Encoding: gzip" header for compressed data. diff --git a/doc/examples/auto_csfle_example.py b/doc/examples/auto_csfle_example.py index fce0499a..9927985c 100644 --- a/doc/examples/auto_csfle_example.py +++ b/doc/examples/auto_csfle_example.py @@ -98,5 +98,4 @@ async def main(): if __name__ == "__main__": - loop = asyncio.get_event_loop() - loop.run_until_complete(main()) + asyncio.run(main()) diff --git a/doc/examples/explicit_encryption_automatic_decryption_example.py b/doc/examples/explicit_encryption_automatic_decryption_example.py index d1ebeb41..11095a3a 100644 --- a/doc/examples/explicit_encryption_automatic_decryption_example.py +++ b/doc/examples/explicit_encryption_automatic_decryption_example.py @@ -73,5 +73,4 @@ async def main(): if __name__ == "__main__": - loop = asyncio.get_event_loop() - loop.run_until_complete(main()) + asyncio.run(main()) diff --git a/doc/examples/explicit_encryption_example.py b/doc/examples/explicit_encryption_example.py index f46e659e..e745a0c1 100644 --- a/doc/examples/explicit_encryption_example.py +++ b/doc/examples/explicit_encryption_example.py @@ -65,5 +65,4 @@ async def main(): if __name__ == "__main__": - loop = asyncio.get_event_loop() - loop.run_until_complete(main()) + asyncio.run(main()) diff --git a/doc/examples/server_fle_enforcement_example.py b/doc/examples/server_fle_enforcement_example.py index 3a6a1058..b0e26756 100644 --- a/doc/examples/server_fle_enforcement_example.py +++ b/doc/examples/server_fle_enforcement_example.py @@ -97,5 +97,4 @@ async def main(): if __name__ == "__main__": - loop = asyncio.get_event_loop() - loop.run_until_complete(main()) + asyncio.run(main()) diff --git a/doc/tutorial-asyncio.rst b/doc/tutorial-asyncio.rst index c23a883e..cdefe9eb 100644 --- a/doc/tutorial-asyncio.rst +++ b/doc/tutorial-asyncio.rst @@ -12,14 +12,16 @@ Tutorial: Using Motor With :mod:`asyncio` import pymongo import motor.motor_asyncio import asyncio - db = motor.motor_asyncio.AsyncIOMotorClient().test_database + client = motor.motor_asyncio.AsyncIOMotorClient() + db = client.test_database .. testsetup:: after-inserting-2000-docs import pymongo import motor.motor_asyncio import asyncio - db = motor.motor_asyncio.AsyncIOMotorClient().test_database + client = motor.motor_asyncio.AsyncIOMotorClient() + db = client.test_database pymongo.MongoClient().test_database.test_collection.insert_many( [{'i': i} for i in range(2000)]) @@ -144,7 +146,7 @@ store a document in MongoDB, call :meth:`~AsyncIOMotorCollection.insert_one` in ... >>> >>> import asyncio - >>> loop = asyncio.get_event_loop() + >>> loop = client.get_io_loop() >>> loop.run_until_complete(do_insert()) result ObjectId('...') @@ -166,7 +168,7 @@ Insert documents in large batches with :meth:`~AsyncIOMotorCollection.insert_man ... [{'i': i} for i in range(2000)]) ... print('inserted %d docs' % (len(result.inserted_ids),)) ... - >>> loop = asyncio.get_event_loop() + >>> loop = client.get_io_loop() >>> loop.run_until_complete(do_insert()) inserted 2000 docs @@ -183,7 +185,7 @@ less than 1: ... document = await db.test_collection.find_one({'i': {'$lt': 1}}) ... pprint.pprint(document) ... - >>> loop = asyncio.get_event_loop() + >>> loop = client.get_io_loop() >>> loop.run_until_complete(do_find_one()) {'_id': ObjectId('...'), 'i': 0} @@ -211,7 +213,7 @@ To find all documents with "i" less than 5: ... for document in await cursor.to_list(length=100): ... pprint.pprint(document) ... - >>> loop = asyncio.get_event_loop() + >>> loop = client.get_io_loop() >>> loop.run_until_complete(do_find()) {'_id': ObjectId('...'), 'i': 0} {'_id': ObjectId('...'), 'i': 1} @@ -234,7 +236,7 @@ You can handle one document at a time in an ``async for`` loop: ... async for document in c.find({'i': {'$lt': 2}}): ... pprint.pprint(document) ... - >>> loop = asyncio.get_event_loop() + >>> loop = client.get_io_loop() >>> loop.run_until_complete(do_find()) {'_id': ObjectId('...'), 'i': 0} {'_id': ObjectId('...'), 'i': 1} @@ -250,7 +252,7 @@ You can apply a sort, limit, or skip to a query before you begin iterating: ... async for document in cursor: ... pprint.pprint(document) ... - >>> loop = asyncio.get_event_loop() + >>> loop = client.get_io_loop() >>> loop.run_until_complete(do_find()) {'_id': ObjectId('...'), 'i': 2} {'_id': ObjectId('...'), 'i': 1} @@ -274,7 +276,7 @@ that match a query: ... n = await db.test_collection.count_documents({'i': {'$gt': 1000}}) ... print('%s documents where i > 1000' % n) ... - >>> loop = asyncio.get_event_loop() + >>> loop = client.get_io_loop() >>> loop.run_until_complete(do_count()) 2000 documents in collection 999 documents where i > 1000 @@ -299,7 +301,7 @@ replacement document. The query follows the same syntax as for :meth:`find` or ... new_document = await coll.find_one({'_id': _id}) ... print('document is now %s' % pprint.pformat(new_document)) ... - >>> loop = asyncio.get_event_loop() + >>> loop = client.get_io_loop() >>> loop.run_until_complete(do_replace()) found document: {'_id': ObjectId('...'), 'i': 50} replaced 1 document @@ -322,7 +324,7 @@ operator to set "key" to "value": ... new_document = await coll.find_one({'i': 51}) ... print('document is now %s' % pprint.pformat(new_document)) ... - >>> loop = asyncio.get_event_loop() + >>> loop = client.get_io_loop() >>> loop.run_until_complete(do_update()) updated 1 document document is now {'_id': ObjectId('...'), 'i': 51, 'key': 'value'} @@ -353,7 +355,7 @@ Deleting Documents ... result = await db.test_collection.delete_many({'i': {'$gte': 1000}}) ... print('%s documents after' % (await coll.count_documents({}))) ... - >>> loop = asyncio.get_event_loop() + >>> loop = client.get_io_loop() >>> loop.run_until_complete(do_delete_many()) 2000 documents before calling delete_many() 1000 documents after @@ -373,7 +375,7 @@ the :meth:`~motor.motor_asyncio.AsyncIOMotorDatabase.command` method on ... response = await db.command(SON([("distinct", "test_collection"), ... ("key", "i")])) ... - >>> loop = asyncio.get_event_loop() + >>> loop = client.get_io_loop() >>> loop.run_until_complete(use_distinct_command()) Since the order of command parameters matters, don't use a Python dict to pass diff --git a/motor/core.py b/motor/core.py index d40ca8f8..a50dcff2 100644 --- a/motor/core.py +++ b/motor/core.py @@ -137,7 +137,8 @@ def __init__(self, *args, **kwargs): io_loop = kwargs.pop("io_loop") self._framework.check_event_loop(io_loop) else: - io_loop = self._framework.get_event_loop() + io_loop = None + self._io_loop = io_loop kwargs.setdefault("connect", False) kwargs.setdefault( @@ -146,7 +147,12 @@ def __init__(self, *args, **kwargs): delegate = self.__delegate_class__(*args, **kwargs) super().__init__(delegate) - self.io_loop = io_loop + + @property + def io_loop(self): + if self._io_loop is None: + self._io_loop = self._framework.get_event_loop() + return self._io_loop def get_io_loop(self): return self.io_loop @@ -1069,22 +1075,13 @@ def main(): change_stream.close() # asyncio - from asyncio import get_event_loop - - def main(): - loop = get_event_loop() - task = loop.create_task(watch_collection) - - try: - loop.run_forever() - except KeyboardInterrupt: + try: + asyncio.run(watch_collection) + except KeyboardInterrupt: pass - finally: - if change_stream is not None: - change_stream.close() - - # Prevent "Task was destroyed but it is pending!" - loop.run_until_complete(task) + finally: + if change_stream is not None: + change_stream.close() The :class:`~MotorChangeStream` async iterable blocks until the next change document is returned or an error is raised. If @@ -1946,13 +1943,19 @@ def __init__( if io_loop: self._framework.check_event_loop(io_loop) else: - io_loop = self._framework.get_event_loop() + io_loop = None sync_client = key_vault_client.delegate delegate = self.__delegate_class__( kms_providers, key_vault_namespace, sync_client, codec_options ) super().__init__(delegate) - self.io_loop = io_loop + self._io_loop = io_loop + + @property + def io_loop(self): + if self._io_loop is None: + self._io_loop = self._framework.get_event_loop() + return self._io_loop def get_io_loop(self): return self.io_loop diff --git a/motor/frameworks/asyncio/__init__.py b/motor/frameworks/asyncio/__init__.py index 3b9b8013..ea5c72ea 100644 --- a/motor/frameworks/asyncio/__init__.py +++ b/motor/frameworks/asyncio/__init__.py @@ -37,7 +37,11 @@ def get_event_loop(): - return asyncio.get_event_loop() + try: + return asyncio.get_running_loop() + except RuntimeError: + # Workaround for bugs.python.org/issue39529. + return asyncio.get_event_loop_policy().get_event_loop() def is_event_loop(loop): diff --git a/setup.py b/setup.py index f4be4b46..0ec0d6c7 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,12 @@ import sys -from distutils.cmd import Command -from distutils.errors import DistutilsOptionError + +if sys.version_info[:2] < (3, 10): + from distutils.cmd import Command + from distutils.errors import DistutilsOptionError as OptionError +else: + from setuptools import Command + from setuptools.errors import OptionError + from setuptools import setup @@ -69,7 +75,7 @@ def finalize_options(self): if self.test_suite is None and self.test_module is None: self.test_module = "test" elif self.test_module is not None and self.test_suite is not None: - raise DistutilsOptionError("You may specify a module or suite, but not both") + raise OptionError("You may specify a module or suite, but not both") def run(self): # Installing required packages, running egg_info and build_ext are diff --git a/tox.ini b/tox.ini index 113da473..d1079dfe 100644 --- a/tox.ini +++ b/tox.ini @@ -70,6 +70,8 @@ deps = synchro37: tornado>=6,<7 synchro37: nose +setenv = + PYTHONWARNINGS="error,ignore:The distutils package is deprecated:DeprecationWarning" commands = python --version python setup.py test --xunit-output=xunit-results {posargs}