Merge pull request #4212 from RonnyPfannschmidt/doctest-testmod-has-call
Doctest: hack in handling mock style objects
This commit is contained in:
		
						commit
						5f16ff3acc
					
				| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					Extend Doctest-modules to ignore mock objects.
 | 
				
			||||||
| 
						 | 
					@ -3,17 +3,19 @@ from __future__ import absolute_import
 | 
				
			||||||
from __future__ import division
 | 
					from __future__ import division
 | 
				
			||||||
from __future__ import print_function
 | 
					from __future__ import print_function
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import inspect
 | 
				
			||||||
import platform
 | 
					import platform
 | 
				
			||||||
import sys
 | 
					import sys
 | 
				
			||||||
import traceback
 | 
					import traceback
 | 
				
			||||||
 | 
					from contextlib import contextmanager
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import pytest
 | 
					import pytest
 | 
				
			||||||
from _pytest._code.code import ExceptionInfo
 | 
					from _pytest._code.code import ExceptionInfo
 | 
				
			||||||
from _pytest._code.code import ReprFileLocation
 | 
					from _pytest._code.code import ReprFileLocation
 | 
				
			||||||
from _pytest._code.code import TerminalRepr
 | 
					from _pytest._code.code import TerminalRepr
 | 
				
			||||||
 | 
					from _pytest.compat import safe_getattr
 | 
				
			||||||
from _pytest.fixtures import FixtureRequest
 | 
					from _pytest.fixtures import FixtureRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
DOCTEST_REPORT_CHOICE_NONE = "none"
 | 
					DOCTEST_REPORT_CHOICE_NONE = "none"
 | 
				
			||||||
DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
 | 
					DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
 | 
				
			||||||
DOCTEST_REPORT_CHOICE_NDIFF = "ndiff"
 | 
					DOCTEST_REPORT_CHOICE_NDIFF = "ndiff"
 | 
				
			||||||
| 
						 | 
					@ -346,10 +348,61 @@ def _check_all_skipped(test):
 | 
				
			||||||
        pytest.skip("all tests skipped by +SKIP option")
 | 
					        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):
 | 
					class DoctestModule(pytest.Module):
 | 
				
			||||||
    def collect(self):
 | 
					    def collect(self):
 | 
				
			||||||
        import doctest
 | 
					        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":
 | 
					        if self.fspath.basename == "conftest.py":
 | 
				
			||||||
            module = self.config.pluginmanager._importconftest(self.fspath)
 | 
					            module = self.config.pluginmanager._importconftest(self.fspath)
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
| 
						 | 
					@ -361,7 +414,7 @@ class DoctestModule(pytest.Module):
 | 
				
			||||||
                else:
 | 
					                else:
 | 
				
			||||||
                    raise
 | 
					                    raise
 | 
				
			||||||
        # uses internal doctest module parsing mechanism
 | 
					        # uses internal doctest module parsing mechanism
 | 
				
			||||||
        finder = doctest.DocTestFinder()
 | 
					        finder = MockAwareDocTestFinder()
 | 
				
			||||||
        optionflags = get_optionflags(self)
 | 
					        optionflags = get_optionflags(self)
 | 
				
			||||||
        runner = _get_runner(
 | 
					        runner = _get_runner(
 | 
				
			||||||
            verbose=0,
 | 
					            verbose=0,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1206,3 +1206,22 @@ class TestDoctestReportingOption(object):
 | 
				
			||||||
                "*error: argument --doctest-report: invalid choice: 'obviously_invalid_format' (choose from*"
 | 
					                "*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 *"])
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue