From 0cbd45f581305dba4bee0374c2df51bbf10979a0 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 21 Apr 2022 12:50:37 -0500 Subject: [PATCH] MOTOR-902 [v2.5] Since Python 3.10 asyncio.get_event_loop() is deprecated (#159) --- 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 | 13 ++++-- tox.ini | 2 + 11 files changed, 59 insertions(+), 48 deletions(-) diff --git a/doc/examples/aiohttp_example.py b/doc/examples/aiohttp_example.py index e28a8c77..8603f23c 100644 --- a/doc/examples/aiohttp_example.py +++ b/doc/examples/aiohttp_example.py @@ -39,8 +39,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 b73a833e..90735435 100644 --- a/doc/examples/aiohttp_gridfs_example.py +++ b/doc/examples/aiohttp_gridfs_example.py @@ -33,7 +33,7 @@ async def put_gridfile(): metadata={'contentType': 'text', 'compressed': True}) -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 05cbcdfe..5ca63227 100644 --- a/doc/examples/auto_csfle_example.py +++ b/doc/examples/auto_csfle_example.py @@ -101,5 +101,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 910d97be..c269918c 100644 --- a/doc/examples/explicit_encryption_automatic_decryption_example.py +++ b/doc/examples/explicit_encryption_automatic_decryption_example.py @@ -70,5 +70,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 1e1f6106..96bbcea7 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 6ea2191d..6c595d70 100644 --- a/doc/examples/server_fle_enforcement_example.py +++ b/doc/examples/server_fle_enforcement_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/tutorial-asyncio.rst b/doc/tutorial-asyncio.rst index eebfd760..6e9738c7 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 f1f59a1a..3269a0c1 100644 --- a/motor/core.py +++ b/motor/core.py @@ -149,7 +149,8 @@ class AgnosticClient(AgnosticBaseProperties): 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( @@ -158,7 +159,12 @@ class AgnosticClient(AgnosticBaseProperties): 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 @@ -985,22 +991,13 @@ class AgnosticCollection(AgnosticBaseProperties): 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 @@ -1835,11 +1832,17 @@ class AgnosticClientEncryption(AgnosticBase): 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 e86c2010..5d31a9e8 100644 --- a/motor/frameworks/asyncio/__init__.py +++ b/motor/frameworks/asyncio/__init__.py @@ -39,7 +39,11 @@ CLASS_PREFIX = 'AsyncIO' 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 b7b52497..56f36d24 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 if sys.version_info[:2] < (3, 5): @@ -71,8 +77,7 @@ class test(Command): 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 8b4ee671..f4e2c6c6 100644 --- a/tox.ini +++ b/tox.ini @@ -67,6 +67,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}