-
-
Notifications
You must be signed in to change notification settings - Fork 32.2k
bpo-1812: Fix newline conversion when doctest.testfile loads from a package whose loader has a get_data method. #17385
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
da35562
dd02845
59370c5
b6ad8ee
22094c8
80977d7
74471ec
83c03d8
efabd42
1d56c05
e1f0a2f
4664f1e
860f835
bb836d7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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 @@ def basics(): r""" | |||||||||||||||||||||||||||||||||||||||||
>>> 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 @@ def test_testfile(): r""" | |||||||||||||||||||||||||||||||||||||||||
>>> 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) | ||||||||||||||||||||||||||||||||||||||||||
zware marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
@contextlib.contextmanager | ||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think some of this and the code above can be folded to use stuff from https://github.com/python/cpython/blob/master/Lib/test/test_importlib/fixtures.py like:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The OnSysPath fixture appears to be designed for a unittest test case, not doctest. A standalone version of the add_sys_path context manager might work if combined with install_finder; however, I notice that neither of those clears the path importer cache, and neither one removes the imported test module from sys.modules when the test is done. I don't know enough about the internals of the test framework to know if leaving out those last two items is OK; I put them in to make sure that everything that could possibly be mutated by this particular test was restored afterwards. |
||||||||||||||||||||||||||||||||||||||||||
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 @@ def test_lineendings(): r""" | |||||||||||||||||||||||||||||||||||||||||
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""" | ||||||||||||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
Fix newline handling in doctest.testfile when loading from a package whose | ||
loader has a get_data method. Patch by Peter Donis. |
Uh oh!
There was an error while loading. Please reload this page.