web-dev-qa-db-fra.com

Tapez le résultat de la requête sqlalchemy

Je ne peux pas comprendre quel type d'objet renvoie une requête sqlalchemy.

entries = session.query(Foo.id, Foo.date).all()

Le type de chaque objet dans les entrées semble être sqlalchemy.util._collections.result, mais un rapide from sqlalchemy.util._collections import result dans un interpréteur python déclenche une ImportError.

Ce que j'essaie finalement de faire, c'est de taper cette fonction:

def my_super_function(session: Session) -> ???:
    entries = session.query(Foo.id, Foo.date).all()
    return entries

Que dois-je mettre à la place de ???? mypy (dans ce cas) semble bien avec List[Tuple[int, str]] parce que oui, je peux accéder à mes entrées comme s'il s'agissait de tuples, mais je peux aussi y accéder avec entry.date, par exemple.

10
JPFrancoia

J'ai moi aussi trouvé curieux que la classe ne puisse pas être importée. La réponse est assez longue pendant que je vous ai expliqué comment je l'ai fait, soyez indulgent avec moi.

Query.all() appelle list() sur l'objet Query lui-même:

def all(self):
    """Return the results represented by this ``Query`` as a list.
    This results in an execution of the underlying query.
    """
    return list(self)

... où la liste sera itérée sur l'objet, donc Query.__iter__() :

def __iter__(self):
    context = self._compile_context()
    context.statement.use_labels = True
    if self._autoflush and not self._populate_existing:
        self.session._autoflush()
    return self._execute_and_instances(context)

... renvoie le résultat de la méthode Query._execute_and_instances() :

def _execute_and_instances(self, querycontext):
    conn = self._get_bind_args(
        querycontext, self._connection_from_session, close_with_result=True
    )

    result = conn.execute(querycontext.statement, self._params)
    return loading.instances(querycontext.query, result, querycontext)

Qui exécute la requête et renvoie le résultat de la fonction sqlalchemy.loading.instances() . Dans cette fonction, il y a cette ligne qui s'applique aux requêtes non à entité unique:

keyed_Tuple = util.lightweight_named_Tuple("result", labels)

... et si je colle une print(keyed_Tuple) après cette ligne, elle affiche <class 'sqlalchemy.util._collections.result'>, qui est le type que vous mentionnez ci-dessus. Donc, quel que soit cet objet, il provient de la fonction sqlalchemy.util._collections.lightweight_named_Tuple() :

def lightweight_named_Tuple(name, fields):
    hash_ = (name,) + Tuple(fields)
    tp_cls = _lw_tuples.get(hash_)
    if tp_cls:
        return tp_cls

    tp_cls = type(
        name,
        (_LW,),
        dict(
            [
                (field, _property_getters[idx])
                for idx, field in enumerate(fields)
                if field is not None
            ]
            + [("__slots__", ())]
        ),
    )

    tp_cls._real_fields = fields
    tp_cls._fields = Tuple([f for f in fields if f is not None])

    _lw_tuples[hash_] = tp_cls
    return tp_cls

Donc, la partie clé est cette déclaration :

tp_cls = type(
    name,
    (_LW,),
    dict(
        [
            (field, _property_getters[idx])
            for idx, field in enumerate(fields)
            if field is not None
        ]
        + [("__slots__", ())]
    ),
)

... qui appelle la classe type() intégrée qui, selon les documents:

Avec trois arguments, renvoyez un nouvel objet type. Il s'agit essentiellement d'une forme dynamique de l'instruction de classe.

Et c'est pourquoi vous ne pouvez pas importer la classe sqlalchemy.util._collections.result - car la classe n'est construite qu'au moment de la requête. Je dirais que la raison en est que les noms de colonnes (c'est-à-dire les attributs nommés Tuple) ne sont pas connus jusqu'à ce que la requête soit exécutée).

De documents python la signature de type est: type(name, bases, dict) où:

La chaîne de nom est le nom de la classe et devient l'attribut __name__; les bases Tuple détaille les classes de base et devient l'attribut __bases__; et le dictionnaire dict est l'espace de noms contenant les définitions du corps de classe et est copié dans un dictionnaire standard pour devenir l'attribut __dict__.

Comme vous pouvez le voir, l'argument bases passé à type() dans lightweight_named_Tuple() est (_LW,). Ainsi, tous les types de tuple nommés créés dynamiquement héritent de sqlalchemy.util._collections._LW , qui est une classe que vous pouvez importer:

from sqlalchemy.util._collections import _LW

entries = session.query(Foo.id, Foo.date).all()
for entry in entries:
    assert isinstance(entry, _LW)  # True

... donc je ne sais pas si c'est une bonne forme de taper votre fonction dans une classe interne avec le trait de soulignement principal, mais _LW hérite de sqlalchemy.util._collections.AbstractKeyedTuple, qui hérite lui-même de Tuple. C'est pourquoi votre typage actuel de List[Tuple[int, str]] Fonctionne, car il est une liste de tuples. Faites donc votre choix, _LW, AbstractKeyedTuple, Tuple seraient tous des représentations correctes de ce que votre fonction retourne.

8
SuperShoot