Skip to content

Commit 9387678

Browse files
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. (cherry picked from commit e0b8101) Co-authored-by: Peter Donis <[email protected]>
1 parent ea0eeb8 commit 9387678

File tree

4 files changed

+100
-5
lines changed

4 files changed

+100
-5
lines changed

Lib/doctest.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,13 @@ def _normalize_module(module, depth=2):
211211
else:
212212
raise TypeError("Expected a module, string, or None")
213213

214+
def _newline_convert(data):
215+
# We have two cases to cover and we need to make sure we do
216+
# them in the right order
217+
for newline in ('\r\n', '\r'):
218+
data = data.replace(newline, '\n')
219+
return data
220+
214221
def _load_testfile(filename, package, module_relative, encoding):
215222
if module_relative:
216223
package = _normalize_module(package, 3)
@@ -221,7 +228,7 @@ def _load_testfile(filename, package, module_relative, encoding):
221228
file_contents = file_contents.decode(encoding)
222229
# get_data() opens files as 'rb', so one must do the equivalent
223230
# conversion as universal newlines would do.
224-
return file_contents.replace(os.linesep, '\n'), filename
231+
return _newline_convert(file_contents), filename
225232
with open(filename, encoding=encoding) as f:
226233
return f.read(), filename
227234

Lib/test/test_doctest.py

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@
88
import os
99
import sys
1010
import importlib
11+
import importlib.abc
12+
import importlib.util
1113
import unittest
1214
import tempfile
15+
import shutil
16+
import contextlib
1317

1418
# NOTE: There are some additional tests relating to interaction with
1519
# zipimport in the test_zipimport_support test module.
@@ -437,7 +441,7 @@ def basics(): r"""
437441
>>> tests = finder.find(sample_func)
438442
439443
>>> print(tests) # doctest: +ELLIPSIS
440-
[<DocTest sample_func from ...:21 (1 example)>]
444+
[<DocTest sample_func from ...:25 (1 example)>]
441445
442446
The exact name depends on how test_doctest was invoked, so allow for
443447
leading path components.
@@ -2659,12 +2663,52 @@ def test_testfile(): r"""
26592663
>>> sys.argv = save_argv
26602664
"""
26612665

2666+
class TestImporter(importlib.abc.MetaPathFinder, importlib.abc.ResourceLoader):
2667+
2668+
def find_spec(self, fullname, path, target=None):
2669+
return importlib.util.spec_from_file_location(fullname, path, loader=self)
2670+
2671+
def get_data(self, path):
2672+
with open(path, mode='rb') as f:
2673+
return f.read()
2674+
2675+
class TestHook:
2676+
2677+
def __init__(self, pathdir):
2678+
self.sys_path = sys.path[:]
2679+
self.meta_path = sys.meta_path[:]
2680+
self.path_hooks = sys.path_hooks[:]
2681+
sys.path.append(pathdir)
2682+
sys.path_importer_cache.clear()
2683+
self.modules_before = sys.modules.copy()
2684+
self.importer = TestImporter()
2685+
sys.meta_path.append(self.importer)
2686+
2687+
def remove(self):
2688+
sys.path[:] = self.sys_path
2689+
sys.meta_path[:] = self.meta_path
2690+
sys.path_hooks[:] = self.path_hooks
2691+
sys.path_importer_cache.clear()
2692+
sys.modules.clear()
2693+
sys.modules.update(self.modules_before)
2694+
2695+
2696+
@contextlib.contextmanager
2697+
def test_hook(pathdir):
2698+
hook = TestHook(pathdir)
2699+
try:
2700+
yield hook
2701+
finally:
2702+
hook.remove()
2703+
2704+
26622705
def test_lineendings(): r"""
2663-
*nix systems use \n line endings, while Windows systems use \r\n. Python
2706+
*nix systems use \n line endings, while Windows systems use \r\n, and
2707+
old Mac systems used \r, which Python still recognizes as a line ending. Python
26642708
handles this using universal newline mode for reading files. Let's make
26652709
sure doctest does so (issue 8473) by creating temporary test files using each
2666-
of the two line disciplines. One of the two will be the "wrong" one for the
2667-
platform the test is run on.
2710+
of the three line disciplines. At least one will not match either the universal
2711+
newline \n or os.linesep for the platform the test is run on.
26682712
26692713
Windows line endings first:
26702714
@@ -2687,6 +2731,47 @@ def test_lineendings(): r"""
26872731
TestResults(failed=0, attempted=1)
26882732
>>> os.remove(fn)
26892733
2734+
And finally old Mac line endings:
2735+
2736+
>>> fn = tempfile.mktemp()
2737+
>>> with open(fn, 'wb') as f:
2738+
... f.write(b'Test:\r\r >>> x = 1 + 1\r\rDone.\r')
2739+
30
2740+
>>> doctest.testfile(fn, module_relative=False, verbose=False)
2741+
TestResults(failed=0, attempted=1)
2742+
>>> os.remove(fn)
2743+
2744+
Now we test with a package loader that has a get_data method, since that
2745+
bypasses the standard universal newline handling so doctest has to do the
2746+
newline conversion itself; let's make sure it does so correctly (issue 1812).
2747+
We'll write a file inside the package that has all three kinds of line endings
2748+
in it, and use a package hook to install a custom loader; on any platform,
2749+
at least one of the line endings will raise a ValueError for inconsistent
2750+
whitespace if doctest does not correctly do the newline conversion.
2751+
2752+
>>> dn = tempfile.mkdtemp()
2753+
>>> pkg = os.path.join(dn, "doctest_testpkg")
2754+
>>> os.mkdir(pkg)
2755+
>>> support.create_empty_file(os.path.join(pkg, "__init__.py"))
2756+
>>> fn = os.path.join(pkg, "doctest_testfile.txt")
2757+
>>> with open(fn, 'wb') as f:
2758+
... f.write(
2759+
... b'Test:\r\n\r\n'
2760+
... b' >>> x = 1 + 1\r\n\r\n'
2761+
... b'Done.\r\n'
2762+
... b'Test:\n\n'
2763+
... b' >>> x = 1 + 1\n\n'
2764+
... b'Done.\n'
2765+
... b'Test:\r\r'
2766+
... b' >>> x = 1 + 1\r\r'
2767+
... b'Done.\r'
2768+
... )
2769+
95
2770+
>>> with test_hook(dn):
2771+
... doctest.testfile("doctest_testfile.txt", package="doctest_testpkg", verbose=False)
2772+
TestResults(failed=0, attempted=3)
2773+
>>> shutil.rmtree(dn)
2774+
26902775
"""
26912776

26922777
def test_testmod(): r"""

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,7 @@ Walter Dörwald
402402
Jaromir Dolecek
403403
Zsolt Dollenstein
404404
Brendan Donegan
405+
Peter Donis
405406
Ismail Donmez
406407
Ray Donnelly
407408
Robert Donohue
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix newline handling in doctest.testfile when loading from a package whose
2+
loader has a get_data method. Patch by Peter Donis.

0 commit comments

Comments
 (0)