Alex Luis Arias
Alex Luis Arias

Reputation: 1394

Wrapping a Class' subclass method

My main class is defined as follows:

from pymongo import MongoClient
from credentials import MongoMarketPlaceAuth


client = MongoClient('mongodb://conn_url:conn_port/', ssl=True)
db_connection = client.DB_NAME
db_connection.authenticate(name=MongoMarketPlaceAuth.name, password=MongoMarketPlaceAuth.password)
MarketPlace = db_connection


class MarketPlaceObjects(object):

    def __init__(self, collection, fields={}, default_filter={}):
        self.collection_name = collection
        self.fields = fields
        self.default_filter = default_filter
        if MarketPlace[collection].count() == 0:
            raise Exception('Collection does not exist or has zero objects')
        self.objects = MarketPlace[collection]

I then have a class that inherits my main class:

class Products(MarketPlaceObjects):

    def __init__(self):
        super(Products, self).__init__(collection='FlatProduct')

When used like so:

from products import Products
p = Products()
p.objects.find_one()

A dictionary is returned that describes all aspects of the product. What I would like to do is figure out is how

p.objects.find_one()

instead of returning a dictionary or a list of dictionaries it can return either one Product object (from a single returned dictionary) or a list of Product objects (from a returned list of dictionaries).

I'm having a hard time since I'm unsure how to I can wrap the find_one() or find() methods of PyMongo's collection class with my own Product class.


UPDATE (2017-07-25): This is what I ended up doing. Still needs some optimization:

Generic classes for the marketplace:

class CollectionObjectInstance(object):
    def __init__(self, response):
        for key, value in response.items():
            if isinstance(value, dict):
                self.__dict__[key] = CollectionObjectInstance(value)
            else:
                self.__dict__[key] = value


class CollectionObjectsManager(object):

    serializer = CollectionObjectInstance
    collection = None
    default_projection = {}
    default_filter = {}

    def __init__(self, **kwargs):
        self.__dict__ = kwargs

    def find(self, filter={}, projection={}):
        filter = self.default_filter.update(filter)
        projection = self.default_projection.update(projection)
        res = self.collection.find(filter, projection)
        for o in res:
            yield self.serializer(o)

    def find_one(self, filter={}, projection={}):
        filter = self.default_filter.update(filter)
        projection = self.default_projection.update(projection)
        res = self.collection.find_one(filter, projection)
        return self.serializer(res)


class MarketPlaceCollection(object):

    collection_name = None
    serializer = None
    objects_manager = CollectionObjectsManager

    def __init__(self, *args, **kwargs):
        if self.collection_name is None:
            raise Exception('collection_name must be defined in class')
        if self.serializer is None:
            raise Exception('serializer must be defined in class')
        collection = MarketPlace[self.collection_name]
        if collection.count() == 0:
            raise Exception('Collection does not exist or has zero objects')
        self.collection = collection
        self.objects = self.objects_manager(**self.__dict__, **self.__class__.__dict__)

Product implementation using inheritance:

from marketplace import MarketPlaceCollection, CollectionObjectInstance
from settings import BASE_URL


URL_SUFFIX = 'product/'
CASH_PRICE_FEE = 50
LEASE_TERM_WEEKS = 52


class Product(CollectionObjectInstance):

    def url(self):
        url = BASE_URL + URL_SUFFIX + self.slug
        return url


class Products(MarketPlaceCollection):

    collection_name = 'FlatProduct'
    serializer = Product

Upvotes: 0

Views: 728

Answers (1)

jsbueno
jsbueno

Reputation: 110440

When you call p.objects you get the collection list itself,as attributed on the line self.objects = MarketPlace[collection]. Your Products have no control over methods or attributes inside the .objects attribute anymore - it is an object as returned by pymongo.

So, to have control over methods and attributes of Products.objects, you have to create another class, with your desired methods, and return an object of that class when one tries to retrieve Products.objects.

Although Python has the "property" decorator and the descriptor protocol, and a more sophisticated automation of your objects attribute could make use of them, in this case it can be made in a very straightforward way. Just have another class that receives the collection, and proxies the other attributes to the collection, by implementing __getattr__ in it:

class ObjectList(object):
    def __init__(self, collection, cls):
        self.collection = collection
        self.cls = cls

    def find_one(self, *args, **kw):
        raw_list = self.collection.find_one(*arg, **kw)
        objects = [self.cls(**obj) for obj in raw_list]
        return objects[0] if len(objects) == 1 else objects

    def __getattr__(self, attr):
        """this is called automatically by Python when a
           normal attribute is not found  in this object
        """ 
        return getattr(self.collection, attr)


class MarketPlaceObjects(object):

    def __init__(self, collection, cls, fields=None, default_filter=None):
        self.collection_name = collection
        self.fields = fields if fields else {}
        self.default_filter = default_filter if defaut_filter else {}
        if MarketPlace[collection].count() == 0:
            raise Exception('Collection does not exist or has zero objects')
        self.objects = ObjectList(MarketPlace[collection], cls)

class Products(MarketPlaceObjects):

    def __init__(self):
        super(Products, self).__init__(collection='FlatProduct', cls=Product)

Upvotes: 1

Related Questions