Skip to content

Commit

Permalink
Merge pull request pytest-dev#4212 from RonnyPfannschmidt/doctest-tes…
Browse files Browse the repository at this point in the history
…tmod-has-call

 Doctest: hack in handling mock style objects
  • Loading branch information
nicoddemus authored Jan 10, 2019
2 parents 71a7452 + a6988aa commit 5f16ff3
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 2 deletions.
1 change: 1 addition & 0 deletions changelog/3456.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Extend Doctest-modules to ignore mock objects.
57 changes: 55 additions & 2 deletions src/_pytest/doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@
from __future__ import division
from __future__ import print_function

import inspect
import platform
import sys
import traceback
from contextlib import contextmanager

import pytest
from _pytest._code.code import ExceptionInfo
from _pytest._code.code import ReprFileLocation
from _pytest._code.code import TerminalRepr
from _pytest.compat import safe_getattr
from _pytest.fixtures import FixtureRequest


DOCTEST_REPORT_CHOICE_NONE = "none"
DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
DOCTEST_REPORT_CHOICE_NDIFF = "ndiff"
Expand Down Expand Up @@ -346,10 +348,61 @@ def _check_all_skipped(test):
pytest.skip("all tests skipped by +SKIP option")


def _is_mocked(obj):
"""
returns if a object is possibly a mock object by checking the existence of a highly improbable attribute
"""
return (
safe_getattr(obj, "pytest_mock_example_attribute_that_shouldnt_exist", None)
is not None
)


@contextmanager
def _patch_unwrap_mock_aware():
"""
contextmanager which replaces ``inspect.unwrap`` with a version
that's aware of mock objects and doesn't recurse on them
"""
real_unwrap = getattr(inspect, "unwrap", None)
if real_unwrap is None:
yield
else:

def _mock_aware_unwrap(obj, stop=None):
if stop is None:
return real_unwrap(obj, stop=_is_mocked)
else:
return real_unwrap(obj, stop=lambda obj: _is_mocked(obj) or stop(obj))

inspect.unwrap = _mock_aware_unwrap
try:
yield
finally:
inspect.unwrap = real_unwrap


class DoctestModule(pytest.Module):
def collect(self):
import doctest

class MockAwareDocTestFinder(doctest.DocTestFinder):
"""
a hackish doctest finder that overrides stdlib internals to fix a stdlib bug
https://github.com/pytest-dev/pytest/issues/3456
https://bugs.python.org/issue25532
"""

def _find(self, tests, obj, name, module, source_lines, globs, seen):
if _is_mocked(obj):
return
with _patch_unwrap_mock_aware():

doctest.DocTestFinder._find(
self, tests, obj, name, module, source_lines, globs, seen
)

if self.fspath.basename == "conftest.py":
module = self.config.pluginmanager._importconftest(self.fspath)
else:
Expand All @@ -361,7 +414,7 @@ def collect(self):
else:
raise
# uses internal doctest module parsing mechanism
finder = doctest.DocTestFinder()
finder = MockAwareDocTestFinder()
optionflags = get_optionflags(self)
runner = _get_runner(
verbose=0,
Expand Down
19 changes: 19 additions & 0 deletions testing/test_doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1206,3 +1206,22 @@ def test_doctest_report_invalid(self, testdir):
"*error: argument --doctest-report: invalid choice: 'obviously_invalid_format' (choose from*"
]
)


@pytest.mark.parametrize("mock_module", ["mock", "unittest.mock"])
def test_doctest_mock_objects_dont_recurse_missbehaved(mock_module, testdir):
pytest.importorskip(mock_module)
testdir.makepyfile(
"""
from {mock_module} import call
class Example(object):
'''
>>> 1 + 1
2
'''
""".format(
mock_module=mock_module
)
)
result = testdir.runpytest("--doctest-modules")
result.stdout.fnmatch_lines(["* 1 passed *"])

0 comments on commit 5f16ff3

Please sign in to comment.