pythonsqlalchemyflask-sqlalchemypython-typing

Adding metadata (annotations) to python variables


I have a sqlalchemy class User which inherits from Model.

class Model:
    @declared_attr
    def __tablename__(self):
        return self.__name__.lower()

    _id = Column(Integer, primary_key=True, autoincrement=True)
    in_utc = Column(BigInteger, default=time())
    out_utc = Column(BigInteger, default=config['MAX_UTC'])

    def to_dict(self):
        return {k: v for k, v in vars(self).items() if not isinstance(v, InstanceState)}

class User(declarative_base(), Model):
    email = Column(String)
    password = Column(String)
    name = Column(String)

The reason for the parent class is to add some columns that are common across the tables and also the to_dict() method which creates a dictionary from the columns.

However I do not want the password column to be included when calling user.to_dict().

Is there any way to annotate the password Column (like in Java reflection) so that to_dict() knows to ignore it?

For example:

class User(declarative_base(), Model):
    email = Column(String)

    [IgnoredInOutput()]
    password = Column(String)

    name = Column(String)

I've now overridden to_dict in the User class to remove the password column for this model.


Solution

  • Python 3.9 introduced Annotated Type Hints which can be used to decorate types with context-specific metadata.

    Annotated takes at least two arguments. The first argument is a regular type (e.g. str, int, etc.), and the rest of the arguments is metadata. A type checker will only check the first argument, leaving the interpretation of the metadata to the application. A type hint like Annotated[str, 'ignored'] will be treated equally to str by type checkers.

    For example this will be a solutions with the new Annotated metadata:

    from sqlalchemy import Column, Integer, String, BigInteger
    from sqlalchemy.ext.declarative import declarative_base, declared_attr
    from sqlalchemy.orm.state import InstanceState
    from typing import Annotated, _AnnotatedAlias
    
    Base = declarative_base()
    
    class Model:
        @declared_attr
        def __tablename__(self):
            return self.__name__.lower()
    
        _id = Column(Integer, primary_key=True, autoincrement=True)
        in_utc = Column(BigInteger)
        out_utc = Column(BigInteger)
    
        def to_dict(self):
            return {key: value for key, value in vars(self).items() if not isinstance(value, InstanceState) and key not in [var for var, th in self.__annotations__.items() if isinstance(th, _AnnotatedAlias) and 'ignored' in th.__metadata__]}
    
    class User(Base, Model):
        email: str = Column(String)
        ignored: Annotated[str, 'ignored'] = Column(String)
        ignored_additional: Annotated[str, 'extra', 'ignored'] = Column(String)
        name = Column(String)
    
    if __name__ == "__main__":
        user = User()
        user.name = "username"
        user.ignored = "ignored"
        user.ignored_additional = "ignored with additional metadata"
        user.email = "username@email.com"
        print(user.to_dict())
    

    As you can see the type hints doesn't have to be included and normal type hints are also accepted without affecting the to_dict function. Also other metadata besides ignored can be added.