Изменение трассировки не вызываемого модуля

Я незначительный вкладчик в пакет, где люди должны это делать (Foo.Bar.Bar - это класс):

>>> from Foo.Bar import Bar
>>> s = Bar('a')

Иногда люди делают это по ошибке (Foo.Bar - это модуль):

>>> from Foo import Bar   
>>> s = Bar('a')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'module' object is not callable

Это может показаться простым, но пользователи все еще не могут его отладить, я хотел бы сделать это проще. Я не могу изменить имена Foo или Bar но я хотел бы добавить более информативный трассировку, например:

TypeError("'module' object is not callable, perhaps you meant to call 'Bar.Bar()'")

Я читаю модули Qable и Callable, и я знаю, что я не могу добавить метод __call__ в модуль (и я не хочу обертывать весь модуль в класс именно для этого). Во всяком случае, я не хочу, чтобы модуль вызывался, я просто хочу настроить трассировку. Есть ли чистое решение для Python 3.x и 2. 7+?

Ответ 1

Есть много способов получить другое сообщение об ошибке, но все они имеют странные оговорки и побочные эффекты.

  • Замена модуля __class__ на types.ModuleType Подкласс класса types.ModuleType вероятно, самый чистый вариант, но он работает только на Python 3. 5+.

    Помимо ограничения 3. 5+, основные странные побочные эффекты, о которых я думал для этой опции, состоят в том, что модуль будет объявлен вызываемым callable функцией, и перезагрузка модуля снова заменит его класс, если вы не будете осторожны чтобы избежать такой двойной замены.

  • Замена объекта модуля на другой объект работает на версиях до версии 3.5 Python, но очень сложно получить полное право.

    Субмодули, перезагрузка, глобальные переменные, любые функциональные возможности модуля, помимо настраиваемого сообщения об ошибке... все они могут сломаться, если вы пропустите какой-то тонкий аспект реализации. Кроме того, модуль будет объявлен вызываемым callable, как и с заменой __class__.

  • Попытка изменить сообщение исключения после sys.excepthook исключения, например, в sys.excepthook, возможно, но нет хорошего способа сказать, что какой-либо конкретный TypeError пришел от попытки вызвать ваш модуль как функцию.

    Вероятно, лучшее, что вы могли бы сделать, это проверить, что TypeError с 'module' object is not callable сообщением в пространстве имен, где кажется правдоподобным, что ваш модуль был бы вызван - например, если имя Bar привязано к Foo.Bar в Foo.Bar фреймов или глобальных Foo.Bar но у них все еще будет много ложных негативов и ложных срабатываний. Кроме того, замена sys.excepthook не совместима с IPython, и любой используемый вами механизм, вероятно, будет конфликтовать с чем-то.

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

Ответ 2

Добавьте это в Bar.py: (Исходя из этого вопроса)

import sys

this_module = sys.modules[__name__]
class MyModule(sys.modules[__name__].__class__):
    def __call__(self, *a, **k):  # module callable
        raise TypeError("'module' object is not callable, perhaps you meant to call 'Bar.Bar()'")
    def __getattribute__(self, name):
        return this_module.__getattribute__(name)

sys.modules[__name__] = MyModule(__name__)


# the rest of file
class Bar:
    pass

Примечание. Протестировано с помощью python3.6 и python2.7.

Ответ 3

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

Ваша собственная функция может:

  • поиск вызывающего кадра в объекте traceback (который содержит информацию об исключении TypeError и функции, которая делает это),
  • поиск объекта Bar в локальных переменных,
  • изменить сообщение об ошибке, если объект является модулем вместо класса или функции.

В Foo.__init__.py вы можете установить свой excepthook

import inspect
import sys


def _install_foo_excepthook():
    _sys_excepthook = sys.excepthook

    def _foo_excepthook(exc_type, exc_value, exc_traceback):
        if exc_type is TypeError:
            # -- find the last frame (source of the exception)
            tb_frame = exc_traceback
            while tb_frame.tb_next is not None:
                tb_frame = tb_frame.tb_next

            # -- search 'Bar' in the local variable
            f_locals = tb_frame.tb_frame.f_locals
            if 'Bar' in f_locals:
                obj = f_locals['Bar']
                if inspect.ismodule(obj):
                    # -- change the error message
                    exc_value.args = ("'module' object is not callable, perhaps you meant to call 'Foo.Bar.Bar()'",)

        _sys_excepthook(exc_type, exc_value, exc_traceback)

    sys.excepthook = _foo_excepthook


_install_foo_excepthook()

Конечно, вам необходимо обеспечить соблюдение этого алгоритма...

Со следующей демонстрацией:

# coding: utf-8

from Foo import Bar

s = Bar('a')

Ты получаешь:

Traceback (most recent call last):
  File "/path/to/demo_bad.py", line 5, in <module>
    s = Bar('a')
TypeError: 'module' object is not callable, perhaps you meant to call 'Foo.Bar.Bar()'