Отладка в Python: отображение последних N строк

Мне бы хотелось увидеть последние 10 строк, которые были выполнены интерпретатором python до этого исключения:

test_has_perm_in_foobar.py F
Traceback (most recent call last):
  File "/.../test_has_perm_in_foobar.py", line 50, in test_has_perm
    self.assertFalse(check_perm(request, some_object))
  File "/usr/lib/python2.7/unittest/case.py", line 416, in assertFalse
    raise self.failureException(msg)
AssertionError: True is not false

Я хочу посмотреть, где check_perm() вернулся True.

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

Я использую pyCharm, но инструмент, основанный на тексте, также решит мою потребность.

BTW: Пожалуйста, не говорите мне, как использовать отладчик с переходом и входом. Я знаю это.

Вот некоторый код, чтобы проиллюстрировать его.

def check_perm(request, some_object):
    if condition_1:
        return True
    if condition_2:
        return sub_check(some_object)
    if condition_3:
        return sub_check2(some_object)
    ...

Существует несколько способов возврата check_perm() True. Если True было возвращено из-за условия_1, то я хочу увидеть что-то вроде этого

+         if condition_1:
+            return True

Вывод, который я имею в виду, похож на set -x на оболочку.

Update

cgitb, pytest и другие инструменты могут показывать строки перед строкой, где утверждение не выполнено. НО, они отображают только строки текущего файла python. Этот вопрос касается строк, которые были выполнены до утверждения, но охватывающих все файлы. В моем случае я хочу знать, где было создано возвращаемое значение check_perm(). Инструменты pytest, cgitb,... не показывают этого.

То, что я ищу, похоже на set -x на оболочке:

help set

-x Печатать команды и их аргументы по мере их выполнения.

Ответы

Ответ 1

Поскольку я не мог найти решение, я сам написал это:

with trace_function_calls():    
    self.assertFalse(check_perm(request, some_object))

Реализация trace_function_calls():

class trace_function_calls(object):
    depth_symbol = '+'

    def __init__(self, write_method=None, log_lines=True):
        '''
        write_method: A method which gets called for every executed line. Defauls to logger.info

        # Simple example:

        with debugutils.trace_function_calls():
            method_you_want_to_trace()
        '''
        if write_method is None:
            write_method=logger.info
        self.write_method = write_method
        self.log_lines = log_lines

    def __enter__(self):
        self.old = sys.gettrace()
        self.depth = 0
        sys.settrace(self.trace_callback)

    def __exit__(self, type, value, traceback):
        sys.settrace(self.old)

    def trace_callback(self, frame, event, arg):
        # from http://pymotw.com/2/sys/tracing.html#tracing-function-calls
        if event == 'return':
            self.depth -= 1
            return self.trace_callback

        if event == 'line':
            if not self.log_lines:
                return self.trace_callback
        elif event == 'call':
            self.depth += 1
        else:
            # self.write_method('unknown: %s' % event)
            return self.trace_callback

        msg = []
        msg.append(self.depth_symbol * self.depth)

        co = frame.f_code
        func_name = co.co_name
        func_line_no = frame.f_lineno

        func_filename = co.co_filename
        if not is_python_file_from_my_codebase(func_filename):
            return self.trace_callback
        code_line = linecache.getline(func_filename, func_line_no).rstrip()
        msg.append('%s: %s %r on line %s of %s' % (
            event, func_name, code_line, func_line_no, func_filename))
        self.write_method(' '.join(msg))
        return self.trace_callback

PS: Это программное обеспечение с открытым исходным кодом. Если вы хотите создать пакет python, сделайте это, скажите мне, это сделает меня радостным.

Ответ 2

По этой причине я переключил тестирование на pytest.

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

Например, в моем проекте django:

$ py.test --showlocals --tb=long
=============================== test session starts ===============================
platform darwin -- Python 3.5.1, pytest-3.0.3, py-1.4.31, pluggy-0.4.0
Django settings: dj_tg_bot.settings (from ini file)
rootdir: /Users/el/Projects/dj-tg-alpha-bot, inifile: tox.ini
plugins: django-3.0.0, cov-2.4.0
collected 8 items

tests/test_commands.py ....F
tests/test_logger.py .
tests/test_simple.py ..

==================================== FAILURES =====================================
__________________________ TestSimpleCommands.test_start __________________________

self = <tests.test_commands.TestSimpleCommands testMethod=test_start>

    def test_start(self,):
        """
            Test bot accept normally command /start and replies as it should.
            """
>       self._test_message_ok(self.start)

self       = <tests.test_commands.TestSimpleCommands testMethod=test_start>

tests/test_commands.py:56:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <tests.test_commands.TestSimpleCommands testMethod=test_start>
action = {'in': ' /start', 'out': {'parse_mode': 'Markdown', 'reply_markup': '', 'text': 'Welcome'}}
update = <telegram.update.Update object at 0x113e16cf8>, number = 1

    def _test_message_ok(self, action, update=None, number=1):
        if not update:
            update = self.update
        with mock.patch("telegram.bot.Bot.sendMessage", callable=mock.MagicMock()) as mock_send:
            if 'in' in action:
                update.message.text = action['in']
            response = self.client.post(self.webhook_url, update.to_json(), **self.kwargs)
            #  Check response 200 OK
            self.assertEqual(response.status_code, status.HTTP_200_OK)
            #  Check
>           self.assertBotResponse(mock_send, action)

action     = {'in': ' /start', 'out': {'parse_mode': 'Markdown', 'reply_markup': '', 'text': 'Welcome'}}
mock_send  = <MagicMock name='sendMessage' id='4619939344'>
number     = 1
response   = <Response status_code=200, "application/json">
self       = <tests.test_commands.TestSimpleCommands testMethod=test_start>
update     = <telegram.update.Update object at 0x113e16cf8>

../../.pyenv/versions/3.5.1/lib/python3.5/site-packages/telegrambot/test/testcases.py:83:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <tests.test_commands.TestSimpleCommands testMethod=test_start>
mock_send = <MagicMock name='sendMessage' id='4619939344'>
command = {'in': ' /start', 'out': {'parse_mode': 'Markdown', 'reply_markup': '', 'text': 'Welcome'}}

    def assertBotResponse(self, mock_send, command):
>       args, kwargs = mock_send.call_args
E       TypeError: 'NoneType' object is not iterable

command    = {'in': ' /start', 'out': {'parse_mode': 'Markdown', 'reply_markup': '', 'text': 'Welcome'}}
mock_send  = <MagicMock name='sendMessage' id='4619939344'>
self       = <tests.test_commands.TestSimpleCommands testMethod=test_start>

../../.pyenv/versions/3.5.1/lib/python3.5/site-packages/telegrambot/test/testcases.py:61: TypeError
------------------------------ Captured stderr call -------------------------------
Handler not found for {'message': {'from': {'username': 'username_4', 'last_name': 'last_name_4', 'id': 5, 'first_name': 'first_name_4'}, 'chat': {'username': 'username_4', 'last_name': 'last_name_4', 'first_name': 'first_name_4', 'title': 'title_4', 'type': 'private', 'id': 5}, 'text': ' /start', 'message_id': 5, 'date': 1482500826}, 'update_id': 5}
======================= 1 failed, 7 passed in 2.29 seconds ========================
(.env) ✘-1 ~/Projects/dj-tg-alpha-bot [master|✚ 1…8⚑ 12]
16:47 $

Ответ 3

Как насчет cgitb? Вам просто нужно импортировать этот модуль в свой код.

import cgitb
cgitb.enable(format='text')

def f():
    a = 1
    b = 2
    c = 3
    x = 0
    d = a * b * c / x
    return d

if __name__ == "__main__":
    f()

дает:

ZeroDivisionError
Python 3.5.2: /usr/bin/python3
Mon Dec 19 17:42:34 2016

A problem occurred in a Python script.  Here is the sequence of
function calls leading up to the error, in the order they occurred.
 /home/user1/123.py in <module>()
   10     d = a * b * c / x
   11     return x
   12 
   13 if __name__ == "__main__":
   14     f()
f = <function f>
 /home/user1/123.py in f()
    8     c = 3
    9     x = 0
   10     d = a * b * c / x
   11     return x
   12 
d undefined
a = 1
b = 2
c = 3
x = 0
ZeroDivisionError: division by zero
...
The above is a description of an error in a Python program.  Here is
the original traceback:

Traceback (most recent call last):
  File "123.py", line 14, in <module>
    f()
  File "123.py", line 10, in f
    d = a * b * c / x
ZeroDivisionError: division by zero

Ответ 4

Модуль trace имеет совместимую с bourne оболочку set -x как функцию. Параметр trace класса trace.Trace включает трассировку выполнения линии. Этот класс принимает параметр ignoredirs, который используется для игнорирования модулей трассировки или пакетов, расположенных ниже указанного каталога. Я использую его здесь, чтобы отслеживать трассировку модуля unittest.

test_has_perm_in_foobar.py

import sys
import trace
import unittest

from app import check_perm

tracer = trace.Trace(trace=1, ignoredirs=(sys.prefix, sys.exec_prefix))

class Test(unittest.TestCase):
    def test_one(self):
        tracer.runctx('self.assertFalse(check_perm("dummy", 3))', globals(), locals())

if __name__ == '__main__':
    unittest.main()

app.py

def sub_check1(some_object):
    if some_object * 10 == 20:
        return True

def sub_check2(some_object):
    if some_object * 10 == 30:
        return True

def check_perm(request, some_object):
    if some_object == 1:
        return True
    if some_object == 2:
        return sub_check1(some_object)
    if some_object == 3:
        return sub_check2(some_object)

Тест

$ python test_has_perm_in_foobar.py 
 --- modulename: test_has_perm_in_foobar, funcname: <module>
<string>(1):   --- modulename: app, funcname: check_perm
app.py(10):     if some_object == 1:
app.py(12):     if some_object == 2:
app.py(14):     if some_object == 3:
app.py(15):         return sub_check2(some_object)
 --- modulename: app, funcname: sub_check2
app.py(6):     if some_object * 10 == 30:
app.py(7):         return True
F
======================================================================
FAIL: test_one (__main__.Test)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_has_perm_in_foobar.py", line 23, in test_one
    tracer.runctx('self.assertFalse(check_perm("dummy", 3))', globals(), locals())
  File "/usr/lib/python2.7/trace.py", line 513, in runctx
    exec cmd in globals, locals
  File "<string>", line 1, in <module>
AssertionError: True is not false

----------------------------------------------------------------------
Ran 1 test in 0.006s

FAILED (failures=1)

Чтобы сделать код и вывод еще более короткими, просто проследите только нужную функцию.

import trace
import unittest

from app import check_perm

tracer = trace.Trace(trace=1)

class Test(unittest.TestCase):
    def test_one(self):
        self.assertFalse(tracer.runfunc(check_perm, 'dummy', 3))

if __name__ == '__main__':
    unittest.main()

Тест

$ python test_has_perm_in_foobar.py
 --- modulename: app, funcname: check_perm
app.py(10):     if some_object == 1:
app.py(12):     if some_object == 2:
app.py(14):     if some_object == 3:
app.py(15):         return sub_check2(some_object)
 --- modulename: app, funcname: sub_check2
app.py(6):     if some_object * 10 == 30:
app.py(7):         return True
F
======================================================================
FAIL: test_one (__main__.Test)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_has_perm_in_foobar.py", line 19, in test_one
    self.assertFalse(tracer.runfunc(check_perm, 'dummy', 3))
AssertionError: True is not false

----------------------------------------------------------------------
Ran 1 test in 0.005s

FAILED (failures=1)

Ответ 5

Рассматривали ли вы следующий рабочий процесс? Я прочитал ваш BTW, но жесткие правила иногда мешают нам решать наши проблемы (особенно если вы находитесь в ловушке XY), поэтому я собираюсь предложить вам использовать отладчик в любом случае. Я сталкиваюсь с испытаниями, которые терпят неудачу все время. Когда полная трассировка стека имеет решающее значение для решения проблемы, я использую комбинацию pdb и py.test, чтобы получить весь shebang. Учитывая следующую программу...

import pytest

@pytest.mark.A
def test_add():

  a = 1
  b = 2

  add(a,b)

def add(a, b):
  assert a>b

  return a+b

def main():
  add(1,2)
  add(2,1)

if __name__ == "__main__":
  # execute only if run as a script
  main()

Выполнение команды py.test -v -tb=short -m A code.py приводит к следующему выводу...

[email protected] ~/src/python/so-answer-stacktrace: py.test -v --tb=short -m A code.py
============================= test session starts ==============================
platform darwin -- Python 2.7.5 -- pytest-2.5.0 -- /Users/art/.pyenv/versions/2.7.5/bin/python
collected 1 items

code.py:3: test_add FAILED

=================================== FAILURES ===================================
___________________________________ test_add ___________________________________
code.py:9: in test_add
>     add(a,b)
code.py:12: in add
>     assert a>b
E     assert 1 > 2
=========================== 1 failed in 0.01 seconds ===========================

Один простой способ исследовать трассировку стека - это отбросить в тесте pdb отладочную точку, Mark отдельный тест с меткой pytest, вызвать этот тест и проверить стек внутри отладчика. как это...

def add(a, b):
  from pdb import set_trace;set_trace()
  assert a>b

  return a+b

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

[email protected] ~/src/python/so-answer-stacktrace: py.test -v --tb=short -m A code.py
=========================================================================================== test session starts ============================================================================================
platform darwin -- Python 2.7.5 -- pytest-2.5.0 -- /Users/art/.pyenv/versions/2.7.5/bin/python
collected 1 items

code.py:3: test_add
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> PDB set_trace (IO-capturing turned off) >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /Users/art/src/python/so-answer-stacktrace/code.py(13)add()
-> assert a>b
(Pdb)

Если в этот момент я набираю магию w для where и нажимаю enter, я вижу полную трассировку стека во всей ее славе...

(Pdb) w
  /Users/art/.pyenv/versions/2.7.5/bin/py.test(9)<module>()
-> load_entry_point('pytest==2.5.0', 'console_scripts', 'py.test')()
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/config.py(19)main()
-> return config.hook.pytest_cmdline_main(config=config)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(376)__call__()
-> return self._docall(methods, kwargs)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(387)_docall()
-> res = mc.execute()
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(288)execute()
-> res = method(**kwargs)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/main.py(111)pytest_cmdline_main()
-> return wrap_session(config, _main)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/main.py(81)wrap_session()
-> doit(config, session)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/main.py(117)_main()
-> config.hook.pytest_runtestloop(session=session)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(376)__call__()
-> return self._docall(methods, kwargs)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(387)_docall()
-> res = mc.execute()
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(288)execute()
-> res = method(**kwargs)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/main.py(137)pytest_runtestloop()
-> item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(376)__call__()
-> return self._docall(methods, kwargs)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(387)_docall()
-> res = mc.execute()
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(288)execute()
-> res = method(**kwargs)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/runner.py(62)pytest_runtest_protocol()
-> runtestprotocol(item, nextitem=nextitem)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/runner.py(72)runtestprotocol()
-> reports.append(call_and_report(item, "call", log))
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/runner.py(106)call_and_report()
-> call = call_runtest_hook(item, when, **kwds)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/runner.py(124)call_runtest_hook()
-> return CallInfo(lambda: ihook(item=item, **kwds), when=when)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/runner.py(137)__init__()
-> self.result = func()
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/runner.py(124)<lambda>()
-> return CallInfo(lambda: ihook(item=item, **kwds), when=when)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/main.py(161)call_matching_hooks()
-> return hookmethod.pcall(plugins, **kwargs)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(380)pcall()
-> return self._docall(methods, kwargs)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(387)_docall()
-> res = mc.execute()
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(288)execute()
-> res = method(**kwargs)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/runner.py(86)pytest_runtest_call()
-> item.runtest()
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/python.py(1076)runtest()
-> self.ihook.pytest_pyfunc_call(pyfuncitem=self)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/main.py(161)call_matching_hooks()
-> return hookmethod.pcall(plugins, **kwargs)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(380)pcall()
-> return self._docall(methods, kwargs)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(387)_docall()
-> res = mc.execute()
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(288)execute()
-> res = method(**kwargs)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/python.py(188)pytest_pyfunc_call()
-> testfunction(**testargs)
  /Users/art/src/python/so-answer-stacktrace/code.py(9)test_add()
-> add(a,b)
> /Users/art/src/python/so-answer-stacktrace/code.py(13)add()
-> assert a>b
(Pdb)

Я делаю много работы в рамках. pdb + where дает вам все, вплоть до фактической точки входа программы. Вы можете видеть, как там находятся мои функции, а также рамы тестового бегуна. Если это Django или Flask, я бы увидел все фреймы стека, связанные с внутренними элементами этих фреймворков. Его полный пробел для остановки, когда все идет по-настоящему неправильно.

Если у вас есть тест с большим количеством итераций или условных выражений, вы можете снова и снова оказаться повесившимся на одних и тех же линиях. Решение должно быть умным, когда вы выбираете инструмент с помощью pdb. Вложение его в условное положение или приведение в действие итерации/рекурсии с условным (по существу говоря, когда это становится True, а затем приостановить, чтобы я мог проверить, что происходит). Кроме того, pdb позволяет вам просматривать весь контекст среды выполнения (назначения, состояние и т.д.)

В вашем случае это похоже на то, что креативная аппаратура check_perm в порядке.