bpo-1812: Fix newline conversion when doctest.testfile loads from a package whose loader has a get_data method (GH-17385)
This pull request fixes the newline conversion bug originally reported in bpo-1812. When that issue was originally submitted, the open builtin did not default to universal newline mode; now it does, which makes the issue fix simpler, since the only code path that needs to be changed is the one in doctest._load_testfile where the file is loaded from a package whose loader has a get_data method.
diff --git a/Lib/test/test_doctest.py b/Lib/test/test_doctest.py
index aa92777..9e88222 100644
--- a/Lib/test/test_doctest.py
+++ b/Lib/test/test_doctest.py
@@ -8,8 +8,12 @@
import os
import sys
import importlib
+import importlib.abc
+import importlib.util
import unittest
import tempfile
+import shutil
+import contextlib
# NOTE: There are some additional tests relating to interaction with
# zipimport in the test_zipimport_support test module.
@@ -437,7 +441,7 @@
>>> tests = finder.find(sample_func)
>>> print(tests) # doctest: +ELLIPSIS
- [<DocTest sample_func from ...:21 (1 example)>]
+ [<DocTest sample_func from ...:25 (1 example)>]
The exact name depends on how test_doctest was invoked, so allow for
leading path components.
@@ -2663,12 +2667,52 @@
>>> sys.argv = save_argv
"""
+class TestImporter(importlib.abc.MetaPathFinder, importlib.abc.ResourceLoader):
+
+ def find_spec(self, fullname, path, target=None):
+ return importlib.util.spec_from_file_location(fullname, path, loader=self)
+
+ def get_data(self, path):
+ with open(path, mode='rb') as f:
+ return f.read()
+
+class TestHook:
+
+ def __init__(self, pathdir):
+ self.sys_path = sys.path[:]
+ self.meta_path = sys.meta_path[:]
+ self.path_hooks = sys.path_hooks[:]
+ sys.path.append(pathdir)
+ sys.path_importer_cache.clear()
+ self.modules_before = sys.modules.copy()
+ self.importer = TestImporter()
+ sys.meta_path.append(self.importer)
+
+ def remove(self):
+ sys.path[:] = self.sys_path
+ sys.meta_path[:] = self.meta_path
+ sys.path_hooks[:] = self.path_hooks
+ sys.path_importer_cache.clear()
+ sys.modules.clear()
+ sys.modules.update(self.modules_before)
+
+
+@contextlib.contextmanager
+def test_hook(pathdir):
+ hook = TestHook(pathdir)
+ try:
+ yield hook
+ finally:
+ hook.remove()
+
+
def test_lineendings(): r"""
-*nix systems use \n line endings, while Windows systems use \r\n. Python
+*nix systems use \n line endings, while Windows systems use \r\n, and
+old Mac systems used \r, which Python still recognizes as a line ending. Python
handles this using universal newline mode for reading files. Let's make
sure doctest does so (issue 8473) by creating temporary test files using each
-of the two line disciplines. One of the two will be the "wrong" one for the
-platform the test is run on.
+of the three line disciplines. At least one will not match either the universal
+newline \n or os.linesep for the platform the test is run on.
Windows line endings first:
@@ -2691,6 +2735,47 @@
TestResults(failed=0, attempted=1)
>>> os.remove(fn)
+And finally old Mac line endings:
+
+ >>> fn = tempfile.mktemp()
+ >>> with open(fn, 'wb') as f:
+ ... f.write(b'Test:\r\r >>> x = 1 + 1\r\rDone.\r')
+ 30
+ >>> doctest.testfile(fn, module_relative=False, verbose=False)
+ TestResults(failed=0, attempted=1)
+ >>> os.remove(fn)
+
+Now we test with a package loader that has a get_data method, since that
+bypasses the standard universal newline handling so doctest has to do the
+newline conversion itself; let's make sure it does so correctly (issue 1812).
+We'll write a file inside the package that has all three kinds of line endings
+in it, and use a package hook to install a custom loader; on any platform,
+at least one of the line endings will raise a ValueError for inconsistent
+whitespace if doctest does not correctly do the newline conversion.
+
+ >>> dn = tempfile.mkdtemp()
+ >>> pkg = os.path.join(dn, "doctest_testpkg")
+ >>> os.mkdir(pkg)
+ >>> support.create_empty_file(os.path.join(pkg, "__init__.py"))
+ >>> fn = os.path.join(pkg, "doctest_testfile.txt")
+ >>> with open(fn, 'wb') as f:
+ ... f.write(
+ ... b'Test:\r\n\r\n'
+ ... b' >>> x = 1 + 1\r\n\r\n'
+ ... b'Done.\r\n'
+ ... b'Test:\n\n'
+ ... b' >>> x = 1 + 1\n\n'
+ ... b'Done.\n'
+ ... b'Test:\r\r'
+ ... b' >>> x = 1 + 1\r\r'
+ ... b'Done.\r'
+ ... )
+ 95
+ >>> with test_hook(dn):
+ ... doctest.testfile("doctest_testfile.txt", package="doctest_testpkg", verbose=False)
+ TestResults(failed=0, attempted=3)
+ >>> shutil.rmtree(dn)
+
"""
def test_testmod(): r"""