2

So I'm using Motor in my application and I'm looking to create helper functions for interacting with the database, I initially looked into using MongoTurbine but I'm biased against using an ORM, although making routine functions easier to call would be nice.

Let's take an upsert for example:

I could write an upsert using the following:

await collection.update_one({'key': value}, {'set': {'key': value}}, upsert = True)

But it would be easier to use:

await collection.upsert({'key': value}, {'set': {'key': value}})

Or even a set method:

await collection.set({'key': value}, {'key': value})

I have quite a few methods like these and some more involved but if anyone could point me in the right direction that'd be awesome!

The trigger that made me ask this question was I saw there was a document_class argument in the AsyncIOMotorDatabase which allows you to specify the return type of documents. But no real easy way to specify a custom collections class.

I know all of this can be done using the standard conventions, I'm only trying to make things easier as sometimes there can be some long filter, update, projection dicts and I'm just trying to implement my own convention.

edit

Basically to put more context into this I'm looking to make custom functions to reduce the amount of dicts I need to write over and over. Take updating a user's point's for example; I'd like to be able to write a sort of "wrapper" function around the update_one function to improve readability and usability across multiple modules.

Ex:

async def update_set(key, value, **kwargs):
    self.update_one({key: value}, {'$set': kwargs})

await db.users.update_set('username', 'johndoe', foo = 1, bar = 'test')
#vs
await db.users.update_one({'username': 'johndoe'}, {'$set': {'foo': 1, 'bar': 'batz'}})
Jab
  • 26,853
  • 21
  • 75
  • 114

2 Answers2

1

I tried simple subclassing and it doesn’t work:

class MyCollection(motor_asyncio.AsyncIOMotorCollection):
    pass

my_collection = MyCollection(database=db, name='my_collection')

Traceback (most recent call last):
  File "./test.py", line 13, in <module>
    my_collection = MyCollection(database=db, name='my_collection')
  File "/usr/local/lib/python3.6/dist-packages/motor/core.py", line 528, in __init__
    super(self.__class__, self).__init__(delegate)
TypeError: __init__() missing 1 required positional argument: 'name'

But with some tricks it finally works:

#!/usr/bin/python3.6

import pymongo
from motor import motor_asyncio


class MyCollection(motor_asyncio.AsyncIOMotorCollection):
    def __init__(self, *args, **kwargs):
        """Calling __init__ of parent class is failing for some reason"""

    def __new__(cls, *args, **kwargs):
        collection = motor_asyncio.AsyncIOMotorCollection(*args, **kwargs)
        collection.__class__ = MyCollection
        return collection

    def my_method(self, *args, **kwargs):
        print('my method', args, kwargs)


client = motor_asyncio.AsyncIOMotorClient()
db = client.db
my_collection = MyCollection(database=db, name='my_collection')
my_collection.my_method()  # my_method () {}
assert db.my_collection == my_collection  # passes assertion
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
sanyassh
  • 8,100
  • 13
  • 36
  • 70
  • The problem that this string `super(self.__class__, self)` is used in /motor/core.py, line 528, in `__init__`. My answer is an attempt to avoid the recursion. – sanyassh Feb 18 '19 at 18:21
  • Ah, yes, indeed. – Martijn Pieters Feb 18 '19 at 19:16
  • So using this I would have to use `my_collection = MyCollection(database=db, name='my_collection')` and not be able to do `my_collection = db.collection`? Is that what's making it "hacky"? – Jab Feb 18 '19 at 20:20
  • Note: the broken use of `super()` is certainly something worth [reporting to the Motor project](https://github.com/mongodb/motor#how-to-ask-for-help) as a bug. – Martijn Pieters Feb 18 '19 at 20:24
  • Thank you will do! – Jab Feb 18 '19 at 20:30
  • It is "hacky" because of setting `collection.__class__ = MyCollection`. My answer is just about a subclassing AsyncIOMotorCollection. If you want to do `my_collection = db.my_collection` and have after it `my_collection.__class__ == MyCollection` you have to write some wrapper about `db`, as I think – sanyassh Feb 18 '19 at 20:34
  • Seems I will need to, as I want to keep the symantics as close to original as possible. – Jab Feb 18 '19 at 20:39
  • 1
    I created an issue on the matter on jira located [here](https://jira.mongodb.org/browse/MOTOR-315) – Jab Feb 18 '19 at 20:53
0

Although this is not a perfect solution, you can achieve this by writing a wrapper class for db object. In a way, this class extends object of another class instead of the class itself.


db = #get connection 
class Wrapper:
    def __init__(self, obj):
        for key in obj.__dir__():
            # skip internal methods
            if key.startswith("__"):
                continue
            setattr(self, key, getattr(obj, key))

    async def update_set(key, value, **kwargs):
        self.update_one({key: value}, {'$set': kwargs})

db = Wrapper(db)
await db.update_set(...)

Note:

You need to use the wrapped object to have access to update_set method. so it's best to wrap it when connecting.

Community
  • 1
  • 1
  • This doesn't allow me to say `db.collection.update_set(...)` That's how the database works now. I need a way to add functionality to the collections `update_one` method. – Jab Feb 18 '19 at 20:17