pythonflask

Python Flask Class initiating with NoneType despite being passed


I'm working on a flask app and it's been running for a while and I don't often add new users so I don't know exactly when this broke, but recently I tried to add a new user and I'm getting an error when adding a User to the database that appears to be caused by the class not instantiating with the variables set. Here's the base class with __init__:

class User(UserMixin,db.Model):
  __tablename__='users'
  id=db.Column(db.Integer,primary_key=True)
  email = db.Column(db.String(128),unique=True,index=True)
  username=db.Column(db.String(64),unique=True,index=True)
  first_name=db.Column(db.String(64))
  last_name=db.Column(db.String(64))
  about_me=db.Column(db.Text())
  role_id = db.Column(db.Integer,db.ForeignKey('roles.id'))
  password_hash = db.Column(db.String(128))
  confirmed = db.Column(db.Boolean, default=False)
  member_since=db.Column(db.DateTime(),default=datetime.utcnow)
  last_seen=db.Column(db.DateTime(),default=datetime.utcnow)
  avatar_hash = db.Column(db.String(32))
  articles = db.relationship('Article',backref='author',lazy="dynamic")
  followed = db.relationship('Follow',
            foreign_keys=[Follow.follower_id],
            backref=db.backref('follower', lazy='joined'),
            lazy='dynamic',
            cascade='all, delete-orphan')
  followers = db.relationship('Follow',
            foreign_keys=[Follow.followed_id],
            backref=db.backref('followed', lazy='joined'),
            lazy='dynamic',
            cascade='all, delete-orphan')

  def __init__(self, **kwargs):
    super(User, self).__init__(**kwargs)
    print(self.email.lower())

I was able to cut the problem line of code down to getting NoneType error on self.email so I've clipped the init function to just initialize and then try to call the problem variable. Then, I can replicate from flask shell:

>>> u = User(email="test@test.com")
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "<string>", line 4, in __init__
  File "/home/eskimotv/venv/lib/python3.8/site-packages/sqlalchemy/orm/state.py", line 433, in _initialize_instance
    manager.dispatch.init_failure(self, args, kwargs)
  File "/home/eskimotv/venv/lib/python3.8/site-packages/sqlalchemy/util/langhelpers.py", line 68, in __exit__
    compat.raise_(
  File "/home/eskimotv/venv/lib/python3.8/site-packages/sqlalchemy/util/compat.py", line 178, in raise_
    raise exception
  File "/home/eskimotv/venv/lib/python3.8/site-packages/sqlalchemy/orm/state.py", line 430, in _initialize_instance
    return manager.original_init(*mixed[1:], **kwargs)
  File "/home/eskimotv/app/app/models.py", line 109, in __init__
    super(User, self).__init__(**kwargs)
  File "/home/eskimotv/venv/lib/python3.8/site-packages/sqlalchemy/ext/declarative/base.py", line 842, in _declarative_constructor
    setattr(self, k, kwargs[k])
  File "/home/eskimotv/venv/lib/python3.8/site-packages/sqlalchemy/orm/attributes.py", line 272, in __set__
    self.impl.set(
  File "/home/eskimotv/venv/lib/python3.8/site-packages/sqlalchemy/orm/attributes.py", line 865, in set
    value = self.fire_replace_event(
  File "/home/eskimotv/venv/lib/python3.8/site-packages/sqlalchemy/orm/attributes.py", line 873, in fire_replace_event
    value = fn(
  File "/home/eskimotv/venv/lib/python3.8/site-packages/sqlalchemy/orm/events.py", line 2162, in wrap
    fn(target, *arg)
  File "/home/eskimotv/app/app/models.py", line 216, in on_changed_email
    target.avatar_hash = target.gravatar_hash()
  File "/home/eskimotv/app/app/models.py", line 212, in gravatar_hash
    return hashlib.md5(self.email.lower().strip().encode('utf-8')).hexdigest()
AttributeError: 'NoneType' object has no attribute 'lower'

Can anyone help me understand why that variable is not being set in the instance of the object?


Solution

  • Indeed, the email address appears to correspond to None.

    However, the error is thrown on line 216, in the on_changed_email() function, in the models.py file, which calls line 212 in the gravatar_hash() function.
    Is it possible that you are using db.event.listen() to respond to changes in the email column within a static method?

    class User(db.Model):
        __tablename__='users'
        id=db.Column(db.Integer,primary_key=True)
        email = db.Column(db.String(128),unique=True,index=True)
        avatar_hash = db.Column(db.String(32))
    
        def __init__(self, **kwargs):
            super(User, self).__init__(**kwargs)
            # ...
        
        def gravatar_hash(self):
            return hashlib.md5(self.email.lower().encode('utf-8')).hexdigest()
    
        @staticmethod
        def on_changed_email(target, value, oldvalue, initiator):
            # ERROR!!!
            # The attribute `self.email` is currently `None`, 
            # but is expected in `self.gravatar_hash()`.
            target.avatar_hash = target.gravatar_hash()
    
    db.event.listen(User.email, 'set', User.on_changed_email)
    

    The problem here is that at the time self.gravatar_hash() is called, as a result of the event, the self.email attribute is not yet set. The error is thrown.
    Listening to events is therefore not suitable for this attribute.

    It's difficult to say where you need to change your code, but within __init__(), the email attribute should not correspond to None.

    class User(db.Model):
        __tablename__='users'
        id=db.Column(db.Integer,primary_key=True)
        email = db.Column(db.String(128),unique=True,index=True)
        avatar_hash = db.Column(db.String(32))
    
        def __init__(self, **kwargs):
            super(User, self).__init__(**kwargs)
            # ...
            if self.email is not None and self.avatar_hash is None:
                self.avatar_hash = self.gravatar_hash()
        
        def gravatar_hash(self):
            return hashlib.md5(self.email.lower().encode('utf-8')).hexdigest()
    

    There is probably another place where the email can be changed, which requires a call to self.gravatar_hash() with an assignment to self.avatar_hash.

    You might find it helpful to look at Miguel Grinberg's example.

    If the problem doesn't occur in the mentioned place, please reduce your code and add all relevant lines to your example.