Search code examples
pythonmongodbooppython-dataclassespython-attrs

Problem with initiating base with child class while working with python-attrs module


I'm trying to create Model class with attrs module. In my script CollectionModel class is inherited in User class. So logically all the attributes in the CollectionModel should be available in the User. But while trying to create a new instance of User from dictionary (it is possible with attrs) it shows that attributes of CollectionModel are not present in the User

My Script:

from bson import ObjectId
from attrs import asdict, define, field, validators, Factory
import time


@define
class CollectionModel:
    """ Base Class For All Collection Schema"""
    _id : str =   field(converter=str, default=Factory(ObjectId))
    _timestamp : float = field(default=Factory(time.time))

    def get_dict(self):
        return asdict(self)

    def update(self):
        self._timestamp = time.time()

@define
class User(CollectionModel):
    
    username : str = field(factory = str, validator=validators.instance_of(str)) 
    userType : str = field(factory = str, validator=validators.instance_of(str)) 
    password : str = field(factory = str, validator=validators.instance_of(str))



user_object = User()
new_user_object = User(**asdict(user_object))

Here, I'm trying to create a new User object from the user_object. It shows the following error.

TypeError: User.__init__() got an unexpected keyword argument '_id'

I guessed that the parent class is not initiated at first. So I tried to initiate it with the super() function and according to attrs documentation it should be done with __attrs_pre_init__. From the documentation:

The sole reason for the existence of __attrs_pre_init__ is to give users the chance to call super().__init__(), because some subclassing-based APIs require that.

So the modified child class becomes like this.

@define
class User(CollectionModel):
    
    username : str = field(factory = str, validator=validators.instance_of(str)) 
    userType : str = field(factory = str, validator=validators.instance_of(str)) 
    password : str = field(factory = str, validator=validators.instance_of(str))

    def __attrs_pre_init__(self):
        super().__init__()

But the problem still remains. Am I doing the OOP in the wrong way? Or is it just a bug of attrs module?


Solution

  • I've solved that issue and there were two problems. The first one, in python, underscore before an attribute name makes it a private attribute. So while creating a new instance from dictionary _id is an unexpected key.

    The second problem is, I indeed need an _id field as I'm working with MongoDB. In MongoDB document _id is reserved for using as a primary key.

    Now the first problem can be solved by replacing _id to id_ (yes, underscore in the opposite side since id is literally a bad choice for a variable name in python).

    For the second problem I've searched a lot to make an alias for id_ field. But it is not simply easy while working with attrs module. So I've modified the get_dict() method to get a perfect dictionary before throwing my data in MongoDB collection.

    Here is the modified CollectionModel class and User class:

    @define
    class CollectionModel:
        """ Base Class For All Collection Schema"""
        id_: ObjectId =   field(default=Factory(ObjectId))
        timestamp : float = field(default=Factory(time.time))
    
        def get_dict(self):
            d = asdict(self)
            d["_id"] = d["id_"]
            del d["id_"]
            return d
    
        def update(self):
            self.timestamp = time.time()
    
    @define
    class User(CollectionModel):
    
        username : str = field(factory = str, validator=validators.instance_of(str)) 
        userType : str = field(factory = str, validator=validators.instance_of(str)) 
        password : str = field(factory = str, validator=validators.instance_of(str))
    

    Now printing an instance:

    user_object = User()
    print(user_object.get_dict())
    

    Output:

    {'timestamp': 1645131182.421929, 'username': '', 'userType': '', 'password': '', '_id': ObjectId('620eb5ae10f27b87de5be3a9')}