2012-03-01 10 views
7

Estoy usando la extensión declarativa en SQLAlchemy, y noté un error extraño cuando intenté guardar una instancia de una clase mapeada con datos incorrectos (específicamente una columna declarada con nullable = False con un valor de Ninguno).SQLAlchemy plantea Ninguno, provoca TypeError

La clase (simplificado):

class User(Base): 
    __tablename__ = 'users' 

    id = Column(Integer, primary_key=True, autoincrement=True) 
    userid = Column(String(50), unique=True, nullable=False) 

que causa el error (sesión es una sesión SQLAlchemy):

>>> u = User() 
>>> session.add(u) 
>>> session.commit() 

... 

TypeError: exceptions must be old-style classes or derived from BaseException, not NoneType 

Mirando el código que hace que esta excepción, he encontrado (en sqlalchemy. orm.session):

except: 
    transaction.rollback(_capture_exception=True) 
    raise 

La excepción está atrapado en este caso es un sqlalchemy.exc.Oper ationalError. Si cambio a estas líneas:

except Exception as e: 
    transaction.rollback(_capture_exception=True) 
    raise e 

entonces el problema desaparece, y la OperationalError se arrojan en lugar de Ninguno. ¿No debería el código original funcionar en cualquier versión reciente de Python? (Estoy usando 2.7.2) ¿Es este error de alguna manera específico para mi aplicación?

Python 2.7.2

SQLAlchemy 0.7.5

ACTUALIZACIÓN: el error parece ser específica a mi solicitud de alguna manera. Estoy envolviendo un eventlet.db_pool con un motor SQLAlchemy, que parece ser la fuente del problema de alguna manera. Ejecutar mi prueba simple con SQLite en memoria o con el motor MySQL básico no tiene este problema, pero con el db_pool sí lo tiene. caso

prueba: https://gist.github.com/1980584

El rastreo completo es:

Traceback (most recent call last): 
    File "test_case_9525220.py", line 41, in <module> 
    session.commit() 
    File "/usr/local/Cellar/python/2.7.2/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 645, in commit 
    self.transaction.commit() 
    File "/usr/local/Cellar/python/2.7.2/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 313, in commit 
    self._prepare_impl() 
    File "/usr/local/Cellar/python/2.7.2/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 297, in _prepare_impl 
    self.session.flush() 
    File "/usr/local/Cellar/python/2.7.2/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 1547, in flush 
    self._flush(objects) 
    File "/usr/local/Cellar/python/2.7.2/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/sqlalchemy/orm/session.py", line 1635, in _flush 
    raise 
TypeError: exceptions must be old-style classes or derived from BaseException, not NoneType 
+0

¿Qué versión de sqlalchemy estás usando? – Crast

+0

SQLAlchemy 0.7.5 – robbles

+2

qué DBAPI es esto (incluida la versión) y cuál es la naturaleza exacta del error? OperationalError se propaga desde DBAPI. Una prueba de reproducción completa aquí sería la mejor y adjúntela como un ticket a http://www.sqlalchemy.org/trac/newticket – zzzeek

Respuesta

5

Aquí es lo que he descubierto:

  • La excepción (un OperationalError) está bien hasta que la transacción fallida retrocede (en Session._flush()).
  • La reversión de la transacción es manejada por mysqldb a través de eventlet.tpool. Específicamente, se llama a eventlet.tpool.execute, lo que implica crear un eventlet.Event y llamar a su método wait.
  • Mientras espera, ocurren algunas cosas complicadas relacionadas con el hilo, una de ellas es la de buscar una excepción y pasarla al Evento que se va a manejar. Recoge el OperationalError que todavía está en sys.exc_type, y finalmente lo borra en eventlet.event.hubs.hub.BaseHub.switch.
  • El control vuelve a Session._flush, y la excepción se vuelve a generar (utilizando raise), pero en este momento no hay ninguna excepción, por lo que intenta raise None.

Este comportamiento se puede reproducir con un ejemplo sencillo:

from eventlet import tpool 

def m(): 
    pass 

try: 
    raise TypeError 
except: 
    tpool.execute(m) 
    raise 

Es un poco claro exactamente lo que eventlet debería estar haciendo en esta situación, así que no sabemos si el error se debe informar a sqlalchemy o eventlet, o ambos.

La forma más fácil de rectificar es, como ya ha señalado, para cambiar las últimas líneas de sqlalchemy.orm.session.Session._flush de

except Exception: 
     transaction.rollback(_capture_exception=True) 
     raise 

a

except Exception, e: 
     transaction.rollback(_capture_exception=True) 
     raise e 

Editar: he planteado un issue en el rastreador de problemas de eventlet. Sin embargo, podría valer la pena plantearlo en sqlalchemy.

+1

Buen trabajo, ahora tiene sentido. Mi solución funcionó porque grabó la excepción, antes de que eventlet lo despejara en la reversión, luego lo planteó explícitamente. Intentaré plantear un problema en sqlalchemy cuando tenga la oportunidad. – robbles

Cuestiones relacionadas