# Copyright 2014 MongoDB, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Dynamic class-creation for Motor.""" import functools import inspect _class_cache = {} def asynchronize(framework, sync_method, doc=None, wrap_class=None, unwrap_class=None): """Decorate `sync_method` so it returns a Future. The method runs on a thread and resolves the Future when it completes. :Parameters: - `motor_class`: Motor class being created, e.g. MotorClient. - `framework`: An asynchronous framework - `sync_method`: Unbound method of pymongo Collection, Database, MongoClient, etc. - `doc`: Optionally override sync_method's docstring - `wrap_class`: Optional PyMongo class, wrap a returned object of this PyMongo class in the equivalent Motor class - `unwrap_class` Optional Motor class name, unwrap an argument with this Motor class name and pass the wrapped PyMongo object instead """ @functools.wraps(sync_method) def method(self, *args, **kwargs): if unwrap_class is not None: # Don't call isinstance(), not checking subclasses. unwrapped_args = [ obj.delegate if obj.__class__.__name__.endswith((unwrap_class, "MotorClientSession")) else obj for obj in args ] unwrapped_kwargs = { key: ( obj.delegate if obj.__class__.__name__.endswith((unwrap_class, "MotorClientSession")) else obj ) for key, obj in kwargs.items() } else: # For speed, don't call unwrap_args_session/unwrap_kwargs_session. unwrapped_args = [ obj.delegate if obj.__class__.__name__.endswith("MotorClientSession") else obj for obj in args ] unwrapped_kwargs = { key: ( obj.delegate if obj.__class__.__name__.endswith("MotorClientSession") else obj ) for key, obj in kwargs.items() } loop = self.get_io_loop() return framework.run_on_executor( loop, sync_method, self.delegate, *unwrapped_args, **unwrapped_kwargs ) if wrap_class is not None: method = framework.pymongo_class_wrapper(method, wrap_class) method.is_wrap_method = True # For Synchro. # This is for the benefit of motor_extensions.py, which needs this info to # generate documentation with Sphinx. method.is_async_method = True name = sync_method.__name__ method.pymongo_method_name = name if doc is not None: method.__doc__ = doc return method def unwrap_args_session(args): return ( obj.delegate if obj.__class__.__name__.endswith("MotorClientSession") else obj for obj in args ) def unwrap_kwargs_session(kwargs): return { key: (obj.delegate if obj.__class__.__name__.endswith("MotorClientSession") else obj) for key, obj in kwargs.items() } _coro_token = object() def coroutine_annotation(f): """In docs, annotate a function that returns a Future with 'coroutine'. This doesn't affect behavior. """ # Like: # @coroutine_annotation # def method(self): # f.coroutine_annotation = True return f class MotorAttributeFactory(object): """Used by Motor classes to mark attributes that delegate in some way to PyMongo. At module import time, create_class_with_framework calls create_attribute() for each attr to create the final class attribute. """ def __init__(self, doc=None): self.doc = doc def create_attribute(self, cls, attr_name): raise NotImplementedError class Async(MotorAttributeFactory): def __init__(self, attr_name, doc=None): """A descriptor that wraps a PyMongo method, such as insert_one, and returns an asynchronous version of the method that returns a Future. :Parameters: - `attr_name`: The name of the attribute on the PyMongo class, if different from attribute on the Motor class """ super().__init__(doc) self.attr_name = attr_name self.wrap_class = None self.unwrap_class = None def create_attribute(self, cls, attr_name): name = self.attr_name or attr_name method = getattr(cls.__delegate_class__, name) return asynchronize( framework=cls._framework, sync_method=method, doc=self.doc, wrap_class=self.wrap_class, unwrap_class=self.unwrap_class, ) def wrap(self, original_class): self.wrap_class = original_class return self def unwrap(self, class_name): self.unwrap_class = class_name return self class AsyncRead(Async): def __init__(self, attr_name=None, doc=None): """A descriptor that wraps a PyMongo read method like find_one() that returns a Future. """ Async.__init__(self, attr_name=attr_name, doc=doc) class AsyncWrite(Async): def __init__(self, attr_name=None, doc=None): """A descriptor that wraps a PyMongo write method like update_one() that accepts getLastError options and returns a Future. """ Async.__init__(self, attr_name=attr_name, doc=doc) class AsyncCommand(Async): def __init__(self, attr_name=None, doc=None): """A descriptor that wraps a PyMongo command like copy_database() that returns a Future and does not accept getLastError options. """ Async.__init__(self, attr_name=attr_name, doc=doc) class ReadOnlyProperty(MotorAttributeFactory): """Creates a readonly attribute on the wrapped PyMongo object.""" def create_attribute(self, cls, attr_name): def fget(obj): return getattr(obj.delegate, attr_name) if self.doc: doc = self.doc else: doc = getattr(cls.__delegate_class__, attr_name).__doc__ if doc: return property(fget=fget, doc=doc) else: return property(fget=fget) class DelegateMethod(ReadOnlyProperty): """A method on the wrapped PyMongo object that does no I/O and can be called synchronously""" def __init__(self, doc=None): ReadOnlyProperty.__init__(self, doc) self.wrap_class = None def wrap(self, original_class): self.wrap_class = original_class return self def create_attribute(self, cls, attr_name): if self.wrap_class is None: return ReadOnlyProperty.create_attribute(self, cls, attr_name) method = getattr(cls.__delegate_class__, attr_name) original_class = self.wrap_class @functools.wraps(method) def wrapper(self_, *args, **kwargs): result = method(self_.delegate, *args, **kwargs) # Don't call isinstance(), not checking subclasses. if result.__class__ == original_class: # Delegate to the current object to wrap the result. return self_.wrap(result) else: return result if self.doc: wrapper.__doc__ = self.doc wrapper.is_wrap_method = True # For Synchro. return wrapper class MotorCursorChainingMethod(MotorAttributeFactory): def create_attribute(self, cls, attr_name): cursor_method = getattr(cls.__delegate_class__, attr_name) @functools.wraps(cursor_method) def return_clone(self, *args, **kwargs): cursor_method(self.delegate, *args, **kwargs) return self # This is for the benefit of Synchro, and motor_extensions.py return_clone.is_motorcursor_chaining_method = True return_clone.pymongo_method_name = attr_name if self.doc: return_clone.__doc__ = self.doc return return_clone def create_class_with_framework(cls, framework, module_name): motor_class_name = framework.CLASS_PREFIX + cls.__motor_class_name__ cache_key = (cls, motor_class_name, framework) cached_class = _class_cache.get(cache_key) if cached_class: return cached_class new_class = type(str(motor_class_name), (cls,), {}) new_class.__module__ = module_name new_class._framework = framework assert hasattr(new_class, "__delegate_class__") # If we're constructing MotorClient from AgnosticClient, for example, # the method resolution order is (AgnosticClient, AgnosticBase, object). # Iterate over bases looking for attributes and coroutines that must be # replaced with framework-specific ones. for base in reversed(inspect.getmro(cls)): # Turn attribute factories into real methods or descriptors. for name, attr in base.__dict__.items(): if isinstance(attr, MotorAttributeFactory): new_class_attr = attr.create_attribute(new_class, name) setattr(new_class, name, new_class_attr) _class_cache[cache_key] = new_class return new_class