Проведение док-тестов через 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
с этими двумя надвигающимися нижними подчеркиваниями, кричите "глубокие внутренние органы, не трогайте" меня;-)... если он сломается в следующей версии, я бы не удивился. Тем не менее, дело вкуса - этот ответ, возможно, можно считать более "удобным".
Но это именно моя точка зрения: удобство может обойтись огромной ценой с точки зрения отказа от простоты, прозрачности и / или избежания внутренних / недокументированных / нестабильных функций; так что, как урок для всех нас, наименьшая чёрная магия, с которой мы можем сойти (даже ценой отказа от эпсилона удобства здесь и там), тем счастливее мы все будем в конечном счете (и тем счастливее мы сделаем других разработчиков, которым необходимо использовать наши текущие усилия в будущем).