Skip to content

Commit 8e7a2e9

Browse files
committed
Add a loader for doctests to the unittest suite
1 parent 47b6239 commit 8e7a2e9

1 file changed

Lines changed: 177 additions & 0 deletions

File tree

tests/test_doctest.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# coding: utf-8
2+
"""Test doctest contained tests in every file of the module.
3+
"""
4+
import doctest
5+
import importlib
6+
import os
7+
import pkgutil
8+
import sys
9+
import types
10+
import warnings
11+
import tempfile
12+
13+
try:
14+
from unittest import mock
15+
except ImportError:
16+
import mock
17+
18+
import six
19+
20+
import fs
21+
import fs.opener.parse
22+
from fs.memoryfs import MemoryFS
23+
from fs.subfs import ClosingSubFS
24+
from fs.tempfs import TempFS
25+
26+
# --- Mocks ------------------------------------------------------------------
27+
28+
29+
def _home_fs():
30+
"""Create a mock filesystem that matches the XDG user-dirs spec."""
31+
home_fs = MemoryFS()
32+
home_fs.makedir("Desktop")
33+
home_fs.makedir("Documents")
34+
home_fs.makedir("Downloads")
35+
home_fs.makedir("Music")
36+
home_fs.makedir("Pictures")
37+
home_fs.makedir("Public")
38+
home_fs.makedir("Templates")
39+
home_fs.makedir("Videos")
40+
return home_fs
41+
42+
43+
def _open_fs(path):
44+
"""A mock `open_fs` that avoids side effects when running doctests."""
45+
if "://" not in path:
46+
path = "osfs://{}".format(path)
47+
parse_result = fs.opener.parse(path)
48+
if parse_result.protocol == "osfs" and parse_result.resource == "~":
49+
home_fs = _home_fs()
50+
if parse_result.path is not None:
51+
home_fs = home_fs.opendir(parse_result.path, factory=ClosingSubFS)
52+
return home_fs
53+
elif parse_result.protocol in {"ftp", "ftps", "mem"}:
54+
return MemoryFS()
55+
else:
56+
raise RuntimeError("not allowed in doctests: {}".format(path))
57+
58+
59+
def _my_fs(module):
60+
"""Create a mock filesystem to be used in examples."""
61+
my_fs = MemoryFS()
62+
63+
if module == "fs.base":
64+
my_fs.makedir("Desktop")
65+
my_fs.makedir("Videos")
66+
my_fs.touch("Videos/starwars.mov")
67+
my_fs.touch("file.txt")
68+
69+
elif module == "fs.info":
70+
my_fs.touch("foo.tar.gz")
71+
my_fs.settext("foo.py", "print('Hello, world!')")
72+
my_fs.makedir("bar")
73+
74+
elif module in {"fs.walk", "fs.glob"}:
75+
my_fs.makedir("dir1")
76+
my_fs.makedir("dir2")
77+
my_fs.settext("foo.py", "print('Hello, world!')")
78+
my_fs.touch("foo.pyc")
79+
my_fs.settext("bar.py", "print('ok')\n\n# this is a comment\n")
80+
my_fs.touch("bar.pyc")
81+
82+
# # used in `fs.glob`
83+
# home_fs.touch("foo.pyc")
84+
# home_fs.touch("bar.pyc")
85+
return my_fs
86+
87+
88+
def _open(filename, mode="r"):
89+
"""A mock `open` that actually opens a temporary file."""
90+
return tempfile.NamedTemporaryFile(mode="r+" if mode == "r" else mode)
91+
92+
93+
# --- Loader protocol --------------------------------------------------------
94+
95+
96+
def _load_tests_from_module(tests, module, globs, setUp=None, tearDown=None):
97+
"""Load tests from module, iterating through submodules."""
98+
for attr in (getattr(module, x) for x in dir(module) if not x.startswith("_")):
99+
if isinstance(attr, types.ModuleType):
100+
suite = doctest.DocTestSuite(
101+
attr,
102+
globs,
103+
setUp=setUp,
104+
tearDown=tearDown,
105+
optionflags=+doctest.ELLIPSIS,
106+
)
107+
tests.addTests(suite)
108+
return tests
109+
110+
111+
def load_tests(loader, tests, ignore):
112+
"""`load_test` function used by unittest to find the doctests."""
113+
114+
# NB (@althonos): we only test docstrings on Python 3 because it's
115+
# extremely hard to maintain compatibility for both versions without
116+
# extensively hacking `doctest` and `unittest`.
117+
if six.PY2:
118+
return tests
119+
120+
def setUp(self):
121+
warnings.simplefilter("ignore")
122+
self._open_fs_mock = mock.patch.object(fs, "open_fs", new=_open_fs)
123+
self._open_fs_mock.__enter__()
124+
self._ftpfs_mock = mock.patch.object(fs.ftpfs, "FTPFS")
125+
self._ftpfs_mock.__enter__()
126+
127+
def tearDown(self):
128+
self._open_fs_mock.__exit__(None, None, None)
129+
self._ftpfs_mock.__exit__(None, None, None)
130+
warnings.simplefilter(warnings.defaultaction)
131+
132+
# doctests are not compatible with `green`, so we may want to bail out
133+
# early if `green` is running the tests
134+
if sys.argv[0].endswith("green"):
135+
return tests
136+
137+
# recursively traverse all library submodules and load tests from them
138+
packages = [None, fs]
139+
140+
for pkg in iter(packages.pop, None):
141+
for (_, subpkgname, subispkg) in pkgutil.walk_packages(pkg.__path__):
142+
# import the submodule and add it to the tests
143+
module = importlib.import_module(".".join([pkg.__name__, subpkgname]))
144+
145+
# load some useful modules / classes / mocks to the
146+
# globals so that we don't need to explicitly import
147+
# them in the doctests
148+
globs = dict(**module.__dict__)
149+
globs.update(
150+
os=os,
151+
fs=fs,
152+
my_fs=_my_fs(module.__name__),
153+
open=_open,
154+
# NB (@althonos): This allows using OSFS in some examples,
155+
# while not actually opening the real filesystem
156+
OSFS=lambda path: MemoryFS(),
157+
# NB (@althonos): This is for compatibility in `fs.registry`
158+
print_list=lambda path: None,
159+
)
160+
161+
# load the doctests into the unittest test suite
162+
tests.addTests(
163+
doctest.DocTestSuite(
164+
module,
165+
globs=globs,
166+
setUp=setUp,
167+
tearDown=tearDown,
168+
optionflags=+doctest.ELLIPSIS,
169+
)
170+
)
171+
172+
# if the submodule is a package, we need to process its submodules
173+
# as well, so we add it to the package queue
174+
if subispkg:
175+
packages.append(module)
176+
177+
return tests

0 commit comments

Comments
 (0)