Skip to content

Instantly share code, notes, and snippets.

@rduplain
Last active January 26, 2016 03:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rduplain/c403ccc4eb9b17d92c27 to your computer and use it in GitHub Desktop.
Save rduplain/c403ccc4eb9b17d92c27 to your computer and use it in GitHub Desktop.
Investigate Python's doctest with hy (hylang.org).

Can I use doctest with hy (hylang.org)?

Yes, you can, with some limitations.

Writing doctests in hy

hy already supports docstrings, and simple-quoted strings can have newlines in them. Write the doctest using formatting of a Python REPL (not hy):

(defn test-fizzbuzz-with-doctest []
  "
  >>> fizzbuzz_of(1)
  '1'
  >>> fizzbuzz_of(9)
  'fizz'
  >>> fizzbuzz_of(25)
  'buzz'
  >>> fizzbuzz_of(30)
  'fizzbuzz'
  >>>
  "
  ())

You will run into some limitations when writing a doctest:

  • hy does not have """ so a literal " will terminate the docstring.
  • unicode literals? Wrap tests in str(...) or use conditionals when writing code compatible with both Python 2 and Python 3.

Running doctests in hy

Testing with doctest usually starts with a call inside the module:

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

... or a call from the command line:

python -m doctest -v FILE

Neither approach works with hy, because (a) the module is not loaded (i.e. in sys.modules) inside a defmain block and (b) python -m doctest has not yet imported hy so will not recognize any .hy files.

The most intuitive solution would have Python import hy when running python -m doctest, but this is hard in Python. It could potentially be done with a sitecustomize.py written on hy install, or a python-with-hy executable installed as an entry point. These are left as exercises to the reader.

An alternative would be to contribute a hy wrapper, porting or otherwise wrapping the doctest command-line interface, so that something like python -m hy.contrib.doctest FILE or hy -m hy.contrib.doctest FILE would work. (doctester2.py accomplishes this; rename it accordingly)

The enclosed doctest wrapper doctester.py will work:

$ python doctester.py fizzbuzz
Trying:
    fizzbuzz_of(1)
Expecting:
    '1'
ok
Trying:
    fizzbuzz_of(9)
Expecting:
    'fizz'
ok
Trying:
    fizzbuzz_of(25)
Expecting:
    'buzz'
ok
Trying:
    fizzbuzz_of(30)
Expecting:
    'fizzbuzz'
ok
5 items had no tests:
    fizzbuzz
    fizzbuzz.fizzbuzz
    fizzbuzz.fizzbuzz_of
    fizzbuzz.main
    fizzbuzz.test_fizzbuzz_of
1 items passed all tests:
   4 tests in fizzbuzz.test_fizzbuzz_with_doctest
4 tests in 6 items.
4 passed and 0 failed.
Test passed.

About

Investigation used hy master 14c412c (after 0.11.1) on Python 2.7 and 3.5.

pip install git+https://git@github.com/hylang/hy.git@14c412c#egg=hy
import doctest
import importlib
import sys
import hy; hy # noqa -- Requisite to treat .hy files as modules.
def testmods(*args, **keywords):
"Wrapper for doctest.testmod, yielding 0 if success else 1 if failures."
for module_name in args:
module = importlib.import_module(module_name)
failures, _ = doctest.testmod(module, **keywords)
if failures:
yield 1
else:
yield 0
def main(argv=sys.argv):
"Run doctest on argument list of named modules, return 0 if no failures."
return sum(testmods(*argv[1:], verbose=True))
if __name__ == '__main__':
sys.exit(main())
# Full Python doctest command-line program wrapper which can test .hy modules.
import contextlib
import doctest
import sys
import hy; hy # noqa -- Requisite to treat .hy files as modules.
def patch_arg(arg):
"""Patch .hy items in argv to appear to be a .py items.
Python doctest code treats .py files separately from other files, even
though it drops the .py extension and attempts to import the module by
name. Therefore, in order to reuse the doctest command-line entry point in
its entirety, patch the argv list.
hy handles the import of .hy files, so we fool Python doctest
`.endswith(".py")` conditions to behave accordingly.
"""
if arg.endswith('.hy'):
return ''.join((arg[:-3], '.py'))
else:
return arg
@contextlib.contextmanager
def patched_argv():
"""Context manager to patch sys.argv using :func:`patch_arg`."""
original_argv = sys.argv
sys.argv = [patch_arg(arg) for arg in sys.argv]
yield
sys.argv = original_argv
def main():
with patched_argv():
return doctest._test()
if __name__ == '__main__':
sys.exit(main())
;; hy fizzbuzz.hy
;;
;; Developed on hy master 14c412c (after 0.11.1) on Python 3.5.
;;
;; pip install git+https://git@github.com/hylang/hy.git@14c412c#egg=hy
;;
;; To use within Python:
;;
;; $ python
;; >>> import hy # Prerequisite to import a .hy file.
;; >>> import fizzbuzz
(import sys)
(def *fizz* "fizz")
(def *buzz* "buzz")
(defn fizzbuzz [&optional [start 1] [end 101] [fd sys.stdout]]
(for [x (range start end)]
(print (fizzbuzz-of x) :file fd)))
(defn fizzbuzz-of [x &optional [fizz *fizz*] [buzz *buzz*]]
"Convert x to 'fizz', 'buzz', or string representation of x."
(str (cond
[(= (% x 15) 0)
(+ fizz buzz)]
[(= (% x 3) 0)
fizz]
[(= (% x 5) 0)
buzz]
[True
x])))
(defn test-fizzbuzz-of []
(for [(, x expected) [[1 "1"] [9 "fizz"] [25 "buzz"] [30 "fizzbuzz"]]]
(assert (= expected (fizzbuzz-of x)) (.format "{} => {}" x expected))))
(defn test-fizzbuzz-with-doctest []
"
>>> fizzbuzz_of(1)
'1'
>>> fizzbuzz_of(9)
'fizz'
>>> fizzbuzz_of(25)
'buzz'
>>> fizzbuzz_of(30)
'fizzbuzz'
>>>
"
())
(defmain [prog &rest args]
(test-fizzbuzz-of)
(fizzbuzz))
"""py.test plugin to add --doctest-hy-modules option.
This is a simple plugin to extend pytest's doctest plugin to collect and run
doctests found in .hy files. If there's interest, we can publish this as a
fully packaged py.test plugin. As is, it can run as a single .py file.
export PYTHONPATH=.
py.test -p pytest_hy_doctest --doctest-modules --doctest-hy-modules
This will pick up all doctests in .py and .hy files in the current project.
Developed with pytest==2.8.7.
"""
import pytest; pytest
from _pytest.doctest import DoctestModule
def pytest_addoption(parser):
group = parser.getgroup("collect")
group.addoption("--doctest-hy-modules",
action="store_true", default=False,
help="run doctests in all .hy modules",
dest="doctesthymodules")
def pytest_collect_file(path, parent):
config = parent.config
if path.ext == ".hy":
if config.option.doctesthymodules:
return DoctestModule(path, parent)
@rduplain
Copy link
Author

We could put this doctester2.py module at hy/contrib/doctest.py, so that python -m hy.contrib.doctest works. (Note that gist comments do not notify in the same way that GitHub issues do, so ping me on IRC or by email to discuss.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment