293 lines
9.6 KiB
Python
293 lines
9.6 KiB
Python
# 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
|