Проведение док-тестов через iPython и псевдо-консоли

У меня есть довольно простой файл документации:

class Foo():
    """
    >>> 3+2
    5
    """

if __name__ in ("__main__", "__console__"):
    import doctest
    doctest.testmod(verbose=True)

который работает, как и ожидалось, при запуске непосредственно через Python.

Однако в iPython я получаю

1 items had no tests:
    __main__
0 tests in 1 items.
0 passed and 0 failed.
Test passed.

Так как это часть проекта Django, и ему потребуется доступ ко всем соответствующим переменным и таким, что настроен manage.py, я также могу запустить его с помощью модифицированной команды, которая использует code.InteractiveConsole, одним из результатов которого является __name__ устанавливается на '__console__".

С кодом выше я получаю тот же результат, что и с iPython. Я попытался изменить последнюю строку на это:

 this = __import__(__name__)
 doctest.testmod(this, verbose=True)

и я получаю ImportError на __console__, что имеет смысл, я думаю. Это не влияет ни на python, ни на ipython.

Итак, я хотел бы иметь возможность успешно проводить doctests через все эти три метода, особенно через InteractiveConsole, так как я ожидаю, что скоро понадобится магия пони Django.

Просто для пояснения, это то, что я ожидаю:

Trying:
    3+2
Expecting:
    5
ok
1 items had no tests:
    __main__
1 items passed all tests:
   1 tests in __main__.Foo
1 tests in 2 items.
1 passed and 0 failed.
Test passed.

2 ответа

Решение

Следующие работы:

$ ipython
...
In [1]: %run file.py

Trying:
    3+2
Expecting:
    5
ok
1 items had no tests:
    __main__
1 items passed all tests:
   1 tests in __main__.Foo
1 tests in 2 items.
1 passed and 0 failed.
Test passed.

In [2]: 

Я не имею понятия почему ipython file.py не работает. Но вышеупомянутое - по крайней мере обходной путь.

РЕДАКТИРОВАТЬ:

Я нашел причину, почему это не работает. Это довольно просто:

  • Если вы не укажете модуль для тестирования в doctest.testmod(), предполагается, что вы хотите проверить __main__ модуль.
  • Когда IPython выполняет файл, переданный ему в командной строке, __main__ модуль IPython __main__, а не ваш модуль. Таким образом, doctest пытается выполнить doctest в скрипте ввода IPython.

Следующее работает, но чувствует себя немного странно:

if __name__ == '__main__':
    import doctest
    import the_current_module
    doctest.testmod(the_current_module)

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

РЕДАКТИРОВАТЬ 2:

Следующий скрипт ipython_doctest, заставляет ipython вести себя так, как вы хотите:

#! /usr/bin/env bash

echo "__IP.magic_run(\"$1\")" > __ipython_run.py
ipython __ipython_run.py

Скрипт создает скрипт Python, который будет выполнять %run argname в IPython.

Пример:

$ ./ipython_doctest file.py
Trying:
    3+2
Expecting:
    5
ok
1 items had no tests:
    __main__
1 items passed all tests:
   1 tests in __main__.Foo
1 tests in 2 items.
1 passed and 0 failed.
Test passed.
Python 2.5 (r25:51908, Mar  7 2008, 03:27:42) 
Type "copyright", "credits" or "license" for more information.

IPython 0.9.1 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object'. ?object also works, ?? prints more.

In [1]:

Корень проблемы в том, что ipython играет странные шутки с __main__ (через свой FakeModule модуль) так что, к тому времени doctest изучает этот "предполагаемый модуль" через __dict__, Foo там нет - так что doctest не вернется в это.

Вот одно из решений:

class Foo():
    """
    >>> 3+2
    5
    """

if __name__ in ("__main__", "__console__"):
    import doctest, inspect, sys
    m = sys.modules['__main__']
    m.__test__ = dict((n,v) for (n,v) in globals().items()
                            if inspect.isclass(v))
    doctest.testmod(verbose=True)

Это производит в соответствии с просьбой:

$ ipython dot.py 
Trying:
    3+2
Expecting:
    5
ok
1 items had no tests:
    __main__
1 items passed all tests:
   1 tests in __main__.__test__.Foo
1 tests in 2 items.
1 passed and 0 failed.
Test passed.
Python 2.5.1 (r251:54863, Feb  6 2009, 19:02:12) 
  [[ snip snip ]]
In [1]: 

Просто настройка глобальная __test__ не работает, опять же, потому что установка его как глобальный из того, что вы думаете, как __main__ на самом деле не помещает его в __dict__ фактического объекта, который восстанавливается m = sys.modules['__main__'] и последнее именно выражение doctest использует внутренне (на самом деле он использует sys.modules.get, но дополнительные меры предосторожности здесь не нужны, так как мы знаем, что __main__ существует в sys.modules... это просто НЕ тот объект, который вы ожидаете!-).

Также просто настройка m.__test__ = globals() напрямую не работает, по другой причине: doctest проверяет, что значения в __test__ являются строками, функциями, классами или модулями, и без некоторого выбора вы не можете гарантировать, что globals() будет удовлетворять этому условию (на самом деле это не так). Здесь я выбираю только классы, если вы также хотите функции или что-то еще, вы можете использовать or в if пункт в генеэкспорте в пределах dict вызов.

Я не знаю точно, как у вас работает оболочка Django, которая способна выполнить ваш скрипт (как я полагаю python manage.py shell не принимает аргументы, вы должны делать что-то еще, и я не могу точно догадаться, что!-), но подобный подход должен помочь (если ваша оболочка Django использует ipython, по умолчанию, когда доступно, или простой Python): соответствующая настройка __test__ в объекте вы получаете как sys.modules['__main__'] (или же __console__, если это то, что вы затем передаете в doctest.testmod, я думаю) должен работать, так как он имитирует то, что doctest будет делать внутри, чтобы найти ваши тестовые строки.

И, в заключение, философское размышление о дизайне, архитектуре, простоте, прозрачности и "черной магии"...:

Все эти усилия - в основном то, что необходимо для победы над "черной магией", которую ipython (и, возможно, Django, хотя он может просто делегировать эту часть ipython) делает от вашего имени для вашего "удобства"... в любое время, когда две структуры (или больше;-) независимо друг от друга выполняют свою собственную марку черной магии, совместимость может внезапно потребовать значительных усилий и стать чем угодно, НО удобным;-).

Я не говорю, что такое же удобство могло бы быть предоставлено (любым из ipython, django и / или doctests) без чёрной магии, самоанализа, поддельных модулей и т. Д.; дизайнеры и сопровождающие каждого из этих фреймворков - превосходные инженеры, и я ожидаю, что они тщательно выполнили свою домашнюю работу и выполнили только минимальное количество черной магии, которое необходимо для обеспечения удобства пользователя, которое, по их мнению, им было необходимо. Тем не менее, даже в такой ситуации "черная магия" внезапно превращается из удобного сна в кошмар отладки, как только вы захотите сделать что-то даже незначительное за пределами того, что задумал автор фреймворка.

Хорошо, может быть, в этом случае это не совсем кошмар, но я заметил, что этот вопрос был открыт некоторое время, и даже с приманкой щедрости он еще не получил много ответов - хотя у вас теперь есть два ответа, чтобы выбрать от, мой, используя __test__ особенность doctest, @ codeape's использует своеобразный __IP.magic_run Особенность Ironpython. Я предпочитаю мой, потому что он не опирается ни на что внутреннее или недокументированное - __test__ Это документированная особенность doctest, в то время как __IP с этими двумя надвигающимися нижними подчеркиваниями, кричите "глубокие внутренние органы, не трогайте" меня;-)... если он сломается в следующей версии, я бы не удивился. Тем не менее, дело вкуса - этот ответ, возможно, можно считать более "удобным".

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

Другие вопросы по тегам