Почему я не могу поймать это исключение python?

я писал некоторые модули etcd для SaltStack и столкнулся с этой странной проблемой, где это каким-то образом мешает мне поймать исключение, и мне интересно, как это делается. Кажется, конкретно вокруг urllib3.

небольшой скрипт ( не соль ):

import etcd
c = etcd.Client('127.0.0.1', 4001)
print c.read('/test1', wait=True, timeout=2)

и когда мы запускаем его:

[root@alpha utils]# /tmp/etcd_watch.py
Traceback (most recent call last):
  File "/tmp/etcd_watch.py", line 5, in <module>
    print c.read('/test1', wait=True, timeout=2)
  File "/usr/lib/python2.6/site-packages/etcd/client.py", line 481, in read
    timeout=timeout)
  File "/usr/lib/python2.6/site-packages/etcd/client.py", line 788, in api_execute
    cause=e
etcd.EtcdConnectionFailed: Connection to etcd failed due to ReadTimeoutError("HTTPConnectionPool(host='127.0.0.1', port=4001): Read timed out.",)

Ок, давайте поймаем этого мерзавца:

#!/usr/bin/python

import etcd
c = etcd.Client('127.0.0.1', 4001)

try:
  print c.read('/test1', wait=True, timeout=2)
except etcd.EtcdConnectionFailed:
  print 'connect failed'

запустить его:

[root@alpha _modules]# /tmp/etcd_watch.py
connect failed

выглядит хорошо - все работает питон. Так в чем проблема? У меня есть это в модуле соли etcd:

[root@alpha _modules]# cat sjmh.py
import etcd

def test():
  c = etcd.Client('127.0.0.1', 4001)
  try:
    return c.read('/test1', wait=True, timeout=2)
  except etcd.EtcdConnectionFailed:
    return False

и когда мы запуск, что:

[root@alpha _modules]# salt 'alpha' sjmh.test
alpha:
    The minion function caused an exception: Traceback (most recent call last):
      File "/usr/lib/python2.6/site-packages/salt/minion.py", line 1173, in _thread_return
        return_data = func(*args, **kwargs)
      File "/var/cache/salt/minion/extmods/modules/sjmh.py", line 5, in test
        c.read('/test1', wait=True, timeout=2)
      File "/usr/lib/python2.6/site-packages/etcd/client.py", line 481, in read
        timeout=timeout)
      File "/usr/lib/python2.6/site-packages/etcd/client.py", line 769, in api_execute
        _ = response.data
      File "/usr/lib/python2.6/site-packages/urllib3/response.py", line 150, in data
        return self.read(cache_content=True)
      File "/usr/lib/python2.6/site-packages/urllib3/response.py", line 218, in read
        raise ReadTimeoutError(self._pool, None, 'Read timed out.')
    ReadTimeoutError: HTTPConnectionPool(host='127.0.0.1', port=4001): Read timed out.

Hrm, это странно. чтение etcd должно было вернуть etcd.EtcdConnectionFailed. Итак, давайте посмотрим на это дальше. Наш модуль теперь таков:

import etcd

def test():
  c = etcd.Client('127.0.0.1', 4001)
  try:
    return c.read('/test1', wait=True, timeout=2)
  except Exception as e:
    return str(type(e))

и получаем:

[root@alpha _modules]# salt 'alpha' sjmh.test
alpha:
    <class 'urllib3.exceptions.ReadTimeoutError'>

Итак, мы знаем, что мы можем поймать эту штуку. И теперь мы знаем, что он бросил ReadTimeoutError, так что давайте поймаем это. Самая новая версия нашего модуль:

import etcd
import urllib3.exceptions

def test():
  c = etcd.Client('127.0.0.1', 4001)
  try:
    c.read('/test1', wait=True, timeout=2)
  except urllib3.exceptions.ReadTimeoutError as e:
    return 'caught ya!'
  except Exception as e:
    return str(type(e))

и наш тест..

[root@alpha _modules]# salt 'alpha' sjmh.test
alpha:
    <class 'urllib3.exceptions.ReadTimeoutError'>

э, погоди, что? Почему мы его не поймали? Исключения работают, верно.. ?

как насчет того, если мы попытаемся поймать базовый класс из urllib3..

[root@alpha _modules]# cat sjmh.py
import etcd
import urllib3.exceptions

def test():
  c = etcd.Client('127.0.0.1', 4001)
  try:
    c.read('/test1', wait=True, timeout=2)
  except urllib3.exceptions.HTTPError:
    return 'got you this time!'

надеяться и молиться..

[root@alpha _modules]# salt 'alpha' sjmh.test
alpha:
    The minion function caused an exception: Traceback (most recent call last):
      File "/usr/lib/python2.6/site-packages/salt/minion.py", line 1173, in _thread_return
        return_data = func(*args, **kwargs)
      File "/var/cache/salt/minion/extmods/modules/sjmh.py", line 7, in test
        c.read('/test1', wait=True, timeout=2)
      File "/usr/lib/python2.6/site-packages/etcd/client.py", line 481, in read
        timeout=timeout)
      File "/usr/lib/python2.6/site-packages/etcd/client.py", line 769, in api_execute
        _ = response.data
      File "/usr/lib/python2.6/site-packages/urllib3/response.py", line 150, in data
        return self.read(cache_content=True)
      File "/usr/lib/python2.6/site-packages/urllib3/response.py", line 218, in read
        raise ReadTimeoutError(self._pool, None, 'Read timed out.')
    ReadTimeoutError: HTTPConnectionPool(host='127.0.0.1', port=4001): Read timed out.

ВЗРЫВ ВЫ! Хорошо, давайте попробуем другой метод, который возвращает другое исключение etcd. Наш модуль теперь выглядит так:

import etcd

def test():
  c = etcd.Client('127.0.0.1', 4001)
  try:
    c.delete('/')
  except etcd.EtcdRootReadOnly:
    return 'got you this time!'

и наши беги:

[root@alpha _modules]# salt 'alpha' sjmh.test
alpha:
    got you this time!

в качестве заключительного теста я сделал этот модуль, который я могу запустить либо из прямого python, либо как модуль соли..

import etcd
import urllib3

def test():
  c = etcd.Client('127.0.0.1', 4001)
  try:
    c.read('/test1', wait=True, timeout=2)
  except urllib3.exceptions.ReadTimeoutError:
    return 'got you this time!'
  except etcd.EtcdConnectionFailed:
    return 'cant get away from me!'
  except etcd.EtcdException:
    return 'oh no you dont'
  except urllib3.exceptions.HTTPError:
    return 'get back here!'
  except Exception as e:
    return 'HOW DID YOU GET HERE? {0}'.format(type(e))

if __name__ == "__main__":
  print test()

через python:

[root@alpha _modules]# python ./sjmh.py
cant get away from me!

С помощью соли:

[root@alpha _modules]# salt 'alpha' sjmh.test
alpha:
    HOW DID YOU GET HERE? <class 'urllib3.exceptions.ReadTimeoutError'>

Итак, мы можем поймать исключения из etcd, которые он бросает. Но, хотя мы обычно можем поймать urllib3 ReadTimeoutError, когда мы запускаем python-etcd в одиночку, когда я запускаю его через соль, ничто, похоже, не может поймать это исключение urllib3, за исключением предложения "исключение".

я могу это сделать, но мне действительно любопытно, что, черт возьми, делает соль, которая делает это так, что исключение неуловимо. Я никогда не видел этого раньше при работе с python, поэтому мне было бы интересно, как это происходит и как я могу обойти это.

Edit:

так что я, наконец, смог поймать его.

import etcd
import urllib3.exceptions
from urllib3.exceptions import ReadTimeoutError

def test():
  c = etcd.Client('127.0.0.1', 4001)
  try:
    c.read('/test1', wait=True, timeout=2)
  except urllib3.exceptions.ReadTimeoutError:
    return 'caught 1'
  except urllib3.exceptions.HTTPError:
    return 'caught 2'
  except ReadTimeoutError:
    return 'caught 3'
  except etcd.EtcdConnectionFailed as ex:
    return 'cant get away from me!'
  except Exception as ex:
    return 'HOW DID YOU GET HERE? {0}'.format(type(ex))

if __name__ == "__main__":
  print test()

и при запуске:

[root@alpha _modules]# salt 'alpha' sjmh.test
alpha:
    caught 3

он все еще не хотя смысла. Из того, что я знаю об исключениях, возврат должен быть "пойман 1". Почему я должен импортировать имя исключения напрямую, а не просто использовать полное имя класса?

БОЛЬШЕ ПРАВОК!

таким образом, добавление сравнения между двумя классами приводит к "False" - что очевидно, потому что предложение except не работает, поэтому они не могут быть одинаковыми.

я добавил следующее к сценарию, прямо перед тем, как вызвать С. читать().

log.debug(urllib3.exceptions.ReadTimeoutError.__module__)
log.debug(ReadTimeoutError.__module__)

и теперь я получаю это в журнале:

[DEBUG   ] requests.packages.urllib3.exceptions
[DEBUG   ] urllib3.exceptions

Итак, это, по-видимому, является причиной того, что его поймали таким образом. Это также воспроизводимо, просто загрузив etcd и библиотеку запросов и сделав что-то вроде этого:

#!/usr/bin/python

#import requests
import etcd

c = etcd.Client('127.0.0.1', 4001)
c.read("/blah", wait=True, timeout=2)

вы в конечном итоге получите "правильное" исключение - etcd.EtcdConnectionFailed. Однако раскомментируйте "запросы", и вы получите urllib3.исключения.ReadTimeoutError, потому что etcd сейчас больше не ловит исключение.

таким образом, кажется, что при импорте запросов он перезаписывает исключения urllib3, и любой другой модуль, который пытается поймать их, терпит неудачу. Кроме того, похоже, что более новые версии запросов не имеют этой проблемы.

1 ответов


мой ответ ниже немного спекуляции, потому что я не могу доказать это на практике с этими точными библиотеками (для начала я не могу воспроизвести вашу ошибку, так как это также зависит от версий библиотек и как они установлены), но тем не менее показывает один из возможных способов этого:

самый последний пример дает хороший ключ: дело в том, что в разные моменты времени выполнения программы name urllib3.exceptions.ReadTimeoutError может относиться к разным классам. ReadTimeoutError, как и для любого другого модуля в Python, это просто имя в urllib3.exceptions пространство имен и может быть переназначен (но это не значит, что это хорошая идея).

при обращении к этому имени по его полному "пути" - мы гарантированно ссылаемся на фактическое состояние его к тому времени, когда мы обращаемся к нему. Однако, когда мы впервые импортируем его как from urllib3.exceptions import ReadTimeoutError - это приносит имя ReadTimeoutError в пространство имен, которое выполняет импорт, и это имя привязанные к значению urllib3.exceptions.ReadTimeoutError ко времени этого импорта. Теперь, если какой-то другой код переназначает позже значение для urllib3.exceptions.ReadTimeoutError - два (его"текущее"/" последнее " значение и ранее импортированное) могут быть фактически разными, поэтому технически вы можете иметь два разных класса. Теперь, что класс исключения будут поднимать - это зависит от того, как код, который вызывает ошибку, использует его: если они ранее импортировали ReadTimeoutError в их пространство имен-тогда это ("оригинал") будет поднят.

чтобы проверить, если это так можно добавить следующее except ReadTimeoutError блок:

print(urllib3.exceptions.ReadTimeoutError == ReadTimeoutError)

если это выводит False - это доказывает, что к моменту исключения, эти две "ссылки" действительно относятся к разным классам.


упрощенный пример плохой реализации, которая может дать похожий результат:

api.py (правильно разработан и существует счастливо сама):
class MyApiException(Exception):
    pass

def foo():
    raise MyApiException('BOOM!')
apibreaker.py (виноват):
import api

class MyVeryOwnException(Exception):
    # note, this doesn't extend MyApiException,
    # but creates a new "branch" in the hierarhcy
    pass

# DON'T DO THIS AT HOME!
api.MyApiException = MyVeryOwnException
apiuser.py:
import api
from api import MyApiException, foo
import apibreaker

if __name__ == '__main__':
    try:
        foo()
    except MyApiException:
        print("Caught exception of an original class")
    except api.MyApiException:
        print("Caught exception of a reassigned class")

при выполнении:

$ python apiuser.py
Caught exception of a reassigned class

если вы удалите строку import apibreaker - ясно, тогда все вернется на свои места как и должно быть.

это очень упрощенный пример, но достаточно иллюстративный, чтобы показать, что когда класс определен в каком-либо модуле, вновь созданный тип (объект, представляющий сам новый класс) "добавляется" под его объявленное имя класса в пространстве имен модуля. Как и любая другая переменная-ее значение может быть технически изменено. То же самое происходит с функциями.