Исключительное поведение в Python __set_name__ - PullRequest
2 голосов
/ 27 мая 2020

У меня есть дочерний класс, который использует Python 3.6+ __set_name__, чтобы гарантировать, что классы-владельцы аннотировали тип поля, несущего дочерний класс. Если у них нет исключения, вызывается.

Однако любое возникшее исключение всегда перехватывается Python, а вместо него вызывается RuntimeError.

Например:

class Child:
    def __set_name__(self, owner, name):
        raise Exception("OOPS!")

class Owner():
    child = Child()

Результаты в:

Traceback (most recent call last):
  File "<stdin>", line 3, in __set_name__
Exception: OOPS!

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: Error calling __set_name__ on 'Child' instance 'child' in 'Owner'

Это вполне может быть ожидаемым поведением (не удается найти конкретную c ссылку на __set_name__ исключения), но, возможно, также предполагает, что ожидается, что __set_name__ никогда не страдает исключением.

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

Есть ли лучший способ вызвать исключение, которое поддается тестированию, или действительно простой способ проверить исключение, заключенное в RuntimeError, действительно ли тот, который был вызван моим кодом?

Ответы [ 2 ]

2 голосов
/ 27 мая 2020

Итак, поскольку вы получаете целиком «Вышеупомянутое исключение явилось прямой причиной следующего исключения», это означает, что где-то в type (базовый метакласс) есть что-то вроде:

try:
    descr.__set_name__(A, 'attr')
except Exception as e:
    raise RuntimeError(msg) from e

То есть, он использует raise new_exception from original_exception, поэтому вы должны иметь возможность самостоятельно выяснить, что было в исходном исключении, используя атрибут __cause__:

Итак, обратите внимание:

In [1]: class Child:
   ...:     def __set_name__(self, owner, name):
   ...:         raise Exception("OOPS!")
   ...: try:
   ...:     class Owner():
   ...:         child = Child()
   ...: except RuntimeError as e:
   ...:     err = e
   ...:

In [2]: err
Out[2]: RuntimeError("Error calling __set_name__ on 'Child' instance 'child' in 'Owner'")

In [3]: err.__cause__
Out[3]: Exception('OOPS!')

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

Вот ссылка на документацию , которая объясняет это более подробно .

1 голос
/ 27 мая 2020

Вы можете получить доступ к атрибуту __cause__ исключения упаковки, чтобы проверить, происходит ли это из-за вашего поднятого исключения:

try:
    class Child:
        def __set_name__(self, owner, name):
            raise Exception("OOPS!")

    class Owner():
        child = Child()

except RuntimeError as rte:
    assert rte.__cause__.args[0] == "OOPS!"  # or a more appropriate check
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...