SQLAlchemy поднимает None, вызывает TypeError - PullRequest
7 голосов
/ 02 марта 2012

Я использую декларативное расширение в SQLAlchemy, и я заметил странную ошибку, когда попытался сохранить экземпляр сопоставленного класса с неверными данными (в частности, столбец, объявленный с nullable = False со значением None).

Класс (упрощенный):

class User(Base):
    __tablename__ = 'users'

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

Причинение ошибки (сеанс является сеансом SQLAlchemy):

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

...

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

Глядя на код, который вызывает это исключение, я обнаружил (в sqlalchemy.orm.session):

except:
    transaction.rollback(_capture_exception=True)
    raise

Исключением, которое перехватывается в этом случае, является sqlalchemy.exc.OperationalError. Если я изменю эти строки на:

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

затем проблема исчезает, и вместо None выдается ошибка OperationalError. Не должен ли оригинальный код работать в любой недавней версии Python? (Я использую 2.7.2) Эта ошибка как-то специфична для моего приложения?

Python 2.7.2

SQLAlchemy 0.7.5

ОБНОВЛЕНИЕ: похоже, ошибка в некоторой степени относится к моему приложению. Я обертываю файл eventlet.db_pool движком SQLAlchemy, который как-то является источником проблемы. Запуск моего простого теста с использованием SQLite в памяти или базового движка MySQL не имеет этой проблемы, но с db_pool это имеет место.

Контрольный пример: https://gist.github.com/1980584

Полный возврат:

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

1 Ответ

5 голосов
/ 20 марта 2012

Вот что я обнаружил:

  • Исключение (OperationalError) допустимо до тех пор, пока откат транзакции не будет выполнен (в Session._flush()).
  • Откат транзакции обрабатывается mysqldb через eventlet.tpool. В частности, вызывается eventlet.tpool.execute, что включает создание eventlet.Event и вызов его метода wait.
  • Во время ожидания происходит несколько сложных вещей, связанных с потоками, одна из которых проверяет исключение и передает его Событию для обработки. Он берет OperationalError, который все еще находится в sys.exc_type, и в конечном итоге очищает его в eventlet.event.hubs.hub.BaseHub.switch.
  • Элемент управления возвращается к Session._flush, и исключение повторно вызывается (с использованием raise), но на этом этапе исключений нет, поэтому он пытается raise None.

Это поведение можно воспроизвести на простом примере:

from eventlet import tpool

def m(): 
    pass

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

Немного неясно, что именно должен делать eventlet в этой ситуации, поэтому я не знаю, следует ли сообщать об ошибке в sqlalchemy или eventlet, или обоим.

Самый простой способ исправить это, как вы уже заметили, изменить последние несколько строк sqlalchemy.orm.session.Session._flush с

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

до

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

Редактировать: Я поднял проблему на трекере событий Eventlet. Возможно, стоит поднять его и в sqlalchemy.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...