1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
|
import os
import sys
from contextlib import ExitStack, contextmanager
from doctest import ELLIPSIS, REPORT_NDIFF, NORMALIZE_WHITESPACE
from sybil import Sybil
from sybil.parsers.codeblock import PythonCodeBlockParser
from sybil.parsers.doctest import DocTestParser
from tempfile import TemporaryDirectory
from types import ModuleType
import pytest
DOCTEST_FLAGS = ELLIPSIS | NORMALIZE_WHITESPACE | REPORT_NDIFF
@contextmanager
def syspath(directory):
try:
sys.path.insert(0, directory)
yield
finally:
assert sys.path[0] == directory
del sys.path[0]
@contextmanager
def sysmodules():
modules = sys.modules.copy()
try:
yield
finally:
sys.modules = modules
class ExampleModule:
def __init__(self, path):
self.path = path
def __call__(self, contents):
with open(self.path, 'w', encoding='utf-8') as fp:
fp.write(contents)
@pytest.fixture
def example():
with ExitStack() as resources:
tmpdir = resources.enter_context(TemporaryDirectory())
resources.enter_context(sysmodules())
resources.enter_context(syspath(tmpdir))
path = os.path.join(tmpdir, 'example.py')
yield ExampleModule(path)
class DoctestNamespace:
def setup(self, namespace):
# The doctests in .rst files require that they mimic being executed in
# a particular module. The stdlib doctest functionality creates its
# own globals namespace, unattached to any specific module object.
# This causes coordination problems between the apparent globals that
# the doctest sees, and public()'s implementation.
#
# We can't make them the same namespace because doing so violates
# other assumptions in the public() function's code, but we can set
# things up to be close enough for the doctest to pass.
#
# We use two techniques to make this work. First, we create a test
# module and ensure that its string name is assigned to the
# namespace's __name__ attribute. We also ensure that the module by
# that name is in the sys.modules cache (and cleaned up in the
# teardown).
#
# The second thing we need to do is to ensure that the module and the
# namespace the doctest is executed in, share the same list object in
# their __all__ attribute. Now, generally public() will create
# __all__ if it doesn't exist, but we can test that in the unittests,
# so it's good enough to just initialize both name bindings to the
# same list object here.
#
# There is some further discussion in this Sybil ticket:
# https://github.com/cjw296/sybil/issues/21
self._testmod = ModuleType('testmod')
namespace['__name__'] = self._testmod.__name__
sys.modules[self._testmod.__name__] = self._testmod
# Used in the doctests to provide a clean __all__.
def reset():
self._testmod.__all__ = namespace['__all__'] = []
reset()
namespace['reset'] = reset
def teardown(self, namespace):
del sys.modules[self._testmod.__name__]
namespace = DoctestNamespace()
pytest_collect_file = Sybil(
parsers=[
DocTestParser(optionflags=DOCTEST_FLAGS),
PythonCodeBlockParser(),
],
pattern='*.rst',
setup=namespace.setup,
).pytest()
|