diff --git a/Doc/library/zipfile.rst b/Doc/library/zipfile.rst index f7303ea3f0a..72b76a09d39 100644 --- a/Doc/library/zipfile.rst +++ b/Doc/library/zipfile.rst @@ -130,7 +130,7 @@ ZipFile Objects --------------- -.. class:: ZipFile(file, mode='r', compression=ZIP_STORED, allowZip64=False) +.. class:: ZipFile(file, mode='r', compression=ZIP_STORED, allowZip64=True) Open a ZIP file, where *file* can be either a path to a file (a string) or a file-like object. The *mode* parameter should be ``'r'`` to read an existing @@ -147,12 +147,9 @@ ZipFile Objects :const:`ZIP_BZIP2` or :const:`ZIP_LZMA` is specified but the corresponding module (:mod:`zlib`, :mod:`bz2` or :mod:`lzma`) is not available, :exc:`RuntimeError` is also raised. The default is :const:`ZIP_STORED`. If *allowZip64* is - ``True`` zipfile will create ZIP files that use the ZIP64 extensions when - the zipfile is larger than 2 GiB. If it is false (the default) :mod:`zipfile` + ``True`` (the default) zipfile will create ZIP files that use the ZIP64 + extensions when the zipfile is larger than 2 GiB. If it is false :mod:`zipfile` will raise an exception when the ZIP file would require ZIP64 extensions. - ZIP64 extensions are disabled by default because the default :program:`zip` - and :program:`unzip` commands on Unix (the InfoZIP utilities) don't support - these extensions. If the file is created with mode ``'a'`` or ``'w'`` and then :meth:`closed ` without adding any files to the archive, the appropriate @@ -171,6 +168,9 @@ ZipFile Objects .. versionchanged:: 3.3 Added support for :mod:`bzip2 ` and :mod:`lzma` compression. + .. versionchanged:: 3.4 + ZIP64 extensions are enabled by default. + .. method:: ZipFile.close() @@ -374,12 +374,15 @@ PyZipFile Objects The :class:`PyZipFile` constructor takes the same parameters as the :class:`ZipFile` constructor, and one additional parameter, *optimize*. -.. class:: PyZipFile(file, mode='r', compression=ZIP_STORED, allowZip64=False, \ +.. class:: PyZipFile(file, mode='r', compression=ZIP_STORED, allowZip64=True, \ optimize=-1) .. versionadded:: 3.2 The *optimize* parameter. + .. versionchanged:: 3.4 + ZIP64 extensions are enabled by default. + Instances have one method in addition to those of :class:`ZipFile` objects: .. method:: PyZipFile.writepy(pathname, basename='', filterfunc=None) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 06cbae5ce3d..e73eca7ec9f 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -6,7 +6,6 @@ import os import posixpath import re import sys -import time import weakref try: import threading @@ -1076,9 +1075,8 @@ class Path(PurePath): # First try to bump modification time # Implementation note: GNU touch uses the UTIME_NOW option of # the utimensat() / futimens() functions. - t = time.time() try: - self._accessor.utime(self, (t, t)) + self._accessor.utime(self, None) except OSError: # Avoid exception chaining pass diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 6663ffa2833..4108d5e2fd9 100755 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -1391,11 +1391,8 @@ class _BasePathTest(object): # The file mtime should be refreshed by calling touch() again p.touch() st = p.stat() - # Issue #19715: there can be an inconsistency under Windows between - # the timestamp rounding when creating a file, and the timestamp - # rounding done when calling utime(). `delta` makes up for this. - delta = 1e-6 if os.name == 'nt' else 0 - self.assertGreaterEqual(st.st_mtime, old_mtime - delta) + self.assertGreaterEqual(st.st_mtime_ns, old_mtime_ns) + self.assertGreaterEqual(st.st_mtime, old_mtime) # Now with exist_ok=False p = P / 'newfileB' self.assertFalse(p.exists()) @@ -1403,6 +1400,13 @@ class _BasePathTest(object): self.assertTrue(p.exists()) self.assertRaises(OSError, p.touch, exist_ok=False) + def test_touch_nochange(self): + P = self.cls(BASE) + p = P / 'fileA' + p.touch() + with p.open('rb') as f: + self.assertEqual(f.read().strip(), b"this is file A") + def test_mkdir(self): P = self.cls(BASE) p = P / 'newdirA' diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py index 7249b13d4c2..c15ea9fa26e 100644 --- a/Lib/test/test_zipfile.py +++ b/Lib/test/test_zipfile.py @@ -506,12 +506,12 @@ class StoredTestZip64InSmallFiles(AbstractTestZip64InSmallFiles, compression = zipfile.ZIP_STORED def large_file_exception_test(self, f, compression): - with zipfile.ZipFile(f, "w", compression) as zipfp: + with zipfile.ZipFile(f, "w", compression, allowZip64=False) as zipfp: self.assertRaises(zipfile.LargeZipFile, zipfp.write, TESTFN, "another.name") def large_file_exception_test2(self, f, compression): - with zipfile.ZipFile(f, "w", compression) as zipfp: + with zipfile.ZipFile(f, "w", compression, allowZip64=False) as zipfp: self.assertRaises(zipfile.LargeZipFile, zipfp.writestr, "another.name", self.data) diff --git a/Lib/test/test_zipfile64.py b/Lib/test/test_zipfile64.py index a8fb7aba545..498d464db3e 100644 --- a/Lib/test/test_zipfile64.py +++ b/Lib/test/test_zipfile64.py @@ -38,7 +38,7 @@ class TestsWithSourceFile(unittest.TestCase): def zipTest(self, f, compression): # Create the ZIP archive. - zipfp = zipfile.ZipFile(f, "w", compression, allowZip64=True) + zipfp = zipfile.ZipFile(f, "w", compression) # It will contain enough copies of self.data to reach about 6GB of # raw data to store. @@ -92,7 +92,7 @@ class OtherTests(unittest.TestCase): def testMoreThan64kFiles(self): # This test checks that more than 64k files can be added to an archive, # and that the resulting archive can be read properly by ZipFile - zipf = zipfile.ZipFile(TESTFN, mode="w") + zipf = zipfile.ZipFile(TESTFN, mode="w", allowZip64=False) zipf.debug = 100 numfiles = (1 << 16) * 3//2 for i in range(numfiles): diff --git a/Lib/tkinter/test/test_tkinter/test_widgets.py b/Lib/tkinter/test/test_tkinter/test_widgets.py index 166a752105a..4de65313bb5 100644 --- a/Lib/tkinter/test/test_tkinter/test_widgets.py +++ b/Lib/tkinter/test/test_tkinter/test_widgets.py @@ -1,6 +1,7 @@ import unittest import tkinter import os +import sys from test.support import requires from tkinter.test.support import (tcl_version, requires_tcl, @@ -262,6 +263,8 @@ class MenubuttonTest(AbstractLabelTest, unittest.TestCase): test_highlightthickness = StandardOptionsTests.test_highlightthickness + @unittest.skipIf(sys.platform == 'darwin', + 'crashes with Cocoa Tk (issue19733)') def test_image(self): widget = self.create() image = tkinter.PhotoImage('image1') diff --git a/Lib/tkinter/test/widget_tests.py b/Lib/tkinter/test/widget_tests.py index 14b4ca104d5..bd274e60d31 100644 --- a/Lib/tkinter/test/widget_tests.py +++ b/Lib/tkinter/test/widget_tests.py @@ -1,5 +1,7 @@ # Common tests for test_tkinter/test_widgets.py and test_ttk/test_widgets.py +import unittest +import sys import tkinter from tkinter.ttk import setup_master, Scale from tkinter.test.support import (tcl_version, requires_tcl, get_tk_patchlevel, @@ -289,6 +291,8 @@ class StandardOptionsTests: self.checkParam(widget, 'highlightthickness', -2, expected=0, conv=self._conv_pixels) + @unittest.skipIf(sys.platform == 'darwin', + 'crashes with Cocoa Tk (issue19733)') def test_image(self): widget = self.create() self.checkImageParam(widget, 'image') diff --git a/Lib/unittest/loader.py b/Lib/unittest/loader.py index e872fcce9de..808c50eb668 100644 --- a/Lib/unittest/loader.py +++ b/Lib/unittest/loader.py @@ -61,8 +61,9 @@ class TestLoader(object): def loadTestsFromTestCase(self, testCaseClass): """Return a suite of all tests cases contained in testCaseClass""" if issubclass(testCaseClass, suite.TestSuite): - raise TypeError("Test cases should not be derived from TestSuite." \ - " Maybe you meant to derive from TestCase?") + raise TypeError("Test cases should not be derived from " + "TestSuite. Maybe you meant to derive from " + "TestCase?") testCaseNames = self.getTestCaseNames(testCaseClass) if not testCaseNames and hasattr(testCaseClass, 'runTest'): testCaseNames = ['runTest'] @@ -200,6 +201,8 @@ class TestLoader(object): self._top_level_dir = top_level_dir is_not_importable = False + is_namespace = False + tests = [] if os.path.isdir(os.path.abspath(start_dir)): start_dir = os.path.abspath(start_dir) if start_dir != top_level_dir: @@ -213,15 +216,52 @@ class TestLoader(object): else: the_module = sys.modules[start_dir] top_part = start_dir.split('.')[0] - start_dir = os.path.abspath(os.path.dirname((the_module.__file__))) + try: + start_dir = os.path.abspath( + os.path.dirname((the_module.__file__))) + except AttributeError: + # look for namespace packages + try: + spec = the_module.__spec__ + except AttributeError: + spec = None + + if spec and spec.loader is None: + if spec.submodule_search_locations is not None: + is_namespace = True + + for path in the_module.__path__: + if (not set_implicit_top and + not path.startswith(top_level_dir)): + continue + self._top_level_dir = \ + (path.split(the_module.__name__ + .replace(".", os.path.sep))[0]) + tests.extend(self._find_tests(path, + pattern, + namespace=True)) + elif the_module.__name__ in sys.builtin_module_names: + # builtin module + raise TypeError('Can not use builtin modules ' + 'as dotted module names') from None + else: + raise TypeError( + 'don\'t know how to discover from {!r}' + .format(the_module)) from None + if set_implicit_top: - self._top_level_dir = self._get_directory_containing_module(top_part) - sys.path.remove(top_level_dir) + if not is_namespace: + self._top_level_dir = \ + self._get_directory_containing_module(top_part) + sys.path.remove(top_level_dir) + else: + sys.path.remove(top_level_dir) if is_not_importable: raise ImportError('Start directory is not importable: %r' % start_dir) - tests = list(self._find_tests(start_dir, pattern)) + if not is_namespace: + tests = list(self._find_tests(start_dir, pattern)) return self.suiteClass(tests) def _get_directory_containing_module(self, module_name): @@ -254,7 +294,7 @@ class TestLoader(object): # override this method to use alternative matching strategy return fnmatch(path, pattern) - def _find_tests(self, start_dir, pattern): + def _find_tests(self, start_dir, pattern, namespace=False): """Used by discovery. Yields test suites it loads.""" paths = sorted(os.listdir(start_dir)) @@ -287,7 +327,8 @@ class TestLoader(object): raise ImportError(msg % (mod_name, module_dir, expected_dir)) yield self.loadTestsFromModule(module) elif os.path.isdir(full_path): - if not os.path.isfile(os.path.join(full_path, '__init__.py')): + if (not namespace and + not os.path.isfile(os.path.join(full_path, '__init__.py'))): continue load_tests = None @@ -304,7 +345,8 @@ class TestLoader(object): # tests loaded from package file yield tests # recurse into the package - yield from self._find_tests(full_path, pattern) + yield from self._find_tests(full_path, pattern, + namespace=namespace) else: try: yield load_tests(self, tests, pattern) diff --git a/Lib/unittest/test/test_discovery.py b/Lib/unittest/test/test_discovery.py index d4eff400064..6b7b1280f8b 100644 --- a/Lib/unittest/test/test_discovery.py +++ b/Lib/unittest/test/test_discovery.py @@ -1,6 +1,8 @@ import os import re import sys +import types +import builtins from test import support import unittest @@ -173,7 +175,7 @@ class TestDiscovery(unittest.TestCase): self.addCleanup(restore_isdir) _find_tests_args = [] - def _find_tests(start_dir, pattern): + def _find_tests(start_dir, pattern, namespace=None): _find_tests_args.append((start_dir, pattern)) return ['tests'] loader._find_tests = _find_tests @@ -436,7 +438,7 @@ class TestDiscovery(unittest.TestCase): expectedPath = os.path.abspath(os.path.dirname(unittest.test.__file__)) self.wasRun = False - def _find_tests(start_dir, pattern): + def _find_tests(start_dir, pattern, namespace=None): self.wasRun = True self.assertEqual(start_dir, expectedPath) return tests @@ -446,5 +448,79 @@ class TestDiscovery(unittest.TestCase): self.assertEqual(suite._tests, tests) + def test_discovery_from_dotted_path_builtin_modules(self): + + loader = unittest.TestLoader() + + listdir = os.listdir + os.listdir = lambda _: ['test_this_does_not_exist.py'] + isfile = os.path.isfile + isdir = os.path.isdir + os.path.isdir = lambda _: False + orig_sys_path = sys.path[:] + def restore(): + os.path.isfile = isfile + os.path.isdir = isdir + os.listdir = listdir + sys.path[:] = orig_sys_path + self.addCleanup(restore) + + with self.assertRaises(TypeError) as cm: + loader.discover('sys') + self.assertEqual(str(cm.exception), + 'Can not use builtin modules ' + 'as dotted module names') + + def test_discovery_from_dotted_namespace_packages(self): + loader = unittest.TestLoader() + + orig_import = __import__ + package = types.ModuleType('package') + package.__path__ = ['/a', '/b'] + package.__spec__ = types.SimpleNamespace( + loader=None, + submodule_search_locations=['/a', '/b'] + ) + + def _import(packagename, *args, **kwargs): + sys.modules[packagename] = package + return package + + def cleanup(): + builtins.__import__ = orig_import + self.addCleanup(cleanup) + builtins.__import__ = _import + + _find_tests_args = [] + def _find_tests(start_dir, pattern, namespace=None): + _find_tests_args.append((start_dir, pattern)) + return ['%s/tests' % start_dir] + + loader._find_tests = _find_tests + loader.suiteClass = list + suite = loader.discover('package') + self.assertEqual(suite, ['/a/tests', '/b/tests']) + + def test_discovery_failed_discovery(self): + loader = unittest.TestLoader() + package = types.ModuleType('package') + orig_import = __import__ + + def _import(packagename, *args, **kwargs): + sys.modules[packagename] = package + return package + + def cleanup(): + builtins.__import__ = orig_import + self.addCleanup(cleanup) + builtins.__import__ = _import + + with self.assertRaises(TypeError) as cm: + loader.discover('package') + self.assertEqual(str(cm.exception), + 'don\'t know how to discover from {!r}' + .format(package)) + + if __name__ == '__main__': unittest.main() diff --git a/Lib/unittest/test/testmock/testmock.py b/Lib/unittest/test/testmock/testmock.py index ac0efeb424d..20cc6541e66 100644 --- a/Lib/unittest/test/testmock/testmock.py +++ b/Lib/unittest/test/testmock/testmock.py @@ -1302,20 +1302,6 @@ class MockTest(unittest.TestCase): self.assertEqual(m.method_calls, []) - def test_attribute_deletion(self): - # this behaviour isn't *useful*, but at least it's now tested... - for Klass in Mock, MagicMock, NonCallableMagicMock, NonCallableMock: - m = Klass() - original = m.foo - m.foo = 3 - del m.foo - self.assertEqual(m.foo, original) - - new = m.foo = Mock() - del m.foo - self.assertEqual(m.foo, new) - - def test_mock_parents(self): for Klass in Mock, MagicMock: m = Klass() @@ -1379,7 +1365,8 @@ class MockTest(unittest.TestCase): def test_attribute_deletion(self): - for mock in Mock(), MagicMock(): + for mock in (Mock(), MagicMock(), NonCallableMagicMock(), + NonCallableMock()): self.assertTrue(hasattr(mock, 'm')) del mock.m diff --git a/Lib/zipfile.py b/Lib/zipfile.py index ca2611dc79f..ae65b42f70f 100644 --- a/Lib/zipfile.py +++ b/Lib/zipfile.py @@ -876,7 +876,7 @@ class ZipExtFile(io.BufferedIOBase): class ZipFile: """ Class with methods to open, read, write, close, list zip files. - z = ZipFile(file, mode="r", compression=ZIP_STORED, allowZip64=False) + z = ZipFile(file, mode="r", compression=ZIP_STORED, allowZip64=True) file: Either the path to the file, or a file-like object. If it is a path, the file will be opened and closed by ZipFile. @@ -892,7 +892,7 @@ class ZipFile: fp = None # Set here since __del__ checks it _windows_illegal_name_trans_table = None - def __init__(self, file, mode="r", compression=ZIP_STORED, allowZip64=False): + def __init__(self, file, mode="r", compression=ZIP_STORED, allowZip64=True): """Open the ZIP file with mode read "r", write "w" or append "a".""" if mode not in ("r", "w", "a"): raise RuntimeError('ZipFile() requires mode "r", "w", or "a"') @@ -1561,7 +1561,7 @@ class PyZipFile(ZipFile): """Class to create ZIP archives with Python library files and packages.""" def __init__(self, file, mode="r", compression=ZIP_STORED, - allowZip64=False, optimize=-1): + allowZip64=True, optimize=-1): ZipFile.__init__(self, file, mode=mode, compression=compression, allowZip64=allowZip64) self._optimize = optimize @@ -1783,7 +1783,7 @@ def main(args = None): os.path.join(path, nm), os.path.join(zippath, nm)) # else: ignore - with ZipFile(args[1], 'w', allowZip64=True) as zf: + with ZipFile(args[1], 'w') as zf: for src in args[2:]: addToZip(zf, src, os.path.basename(src)) diff --git a/Misc/ACKS b/Misc/ACKS index 9436af8f3f1..8c2aa4d8a19 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -806,6 +806,7 @@ Marek Majkowski Grzegorz Makarewicz David Malcolm Greg Malcolm +William Mallard Ken Manheimer Vladimir Marangozov Colin Marc diff --git a/Misc/NEWS b/Misc/NEWS index b7b9b3b1dd4..2b804e8cabe 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -71,6 +71,12 @@ Library - Issue #19689: Add ssl.create_default_context() factory function. It creates a new SSLContext object with secure default settings. +- Issue #19727: os.utime(..., None) is now potentially more precise + under Windows. + +- Issue #17201: ZIP64 extensions now are enabled by default. Patch by + William Mallard. + - Issue #19292: Add SSLContext.load_default_certs() to load default root CA certificates from default stores or system stores. By default the method loads CA certs for authentication of server certs. @@ -482,6 +488,9 @@ Core and Builtins Library ------- +- Issue #17457: unittest test discovery now works with namespace packages. + Patch by Claudiu Popa. + - Issue #18235: Fix the sysconfig variables LDSHARED and BLDSHARED under AIX. Patch by David Edelsohn. diff --git a/Misc/python-wing5.wpr b/Misc/python-wing5.wpr new file mode 100644 index 00000000000..0e1ae635438 --- /dev/null +++ b/Misc/python-wing5.wpr @@ -0,0 +1,18 @@ +#!wing +#!version=5.0 +################################################################## +# Wing IDE project file # +################################################################## +[project attributes] +proj.directory-list = [{'dirloc': loc('..'), + 'excludes': [u'.hg', + u'Lib/unittest/__pycache__', + u'Lib/unittest/test/__pycache__', + u'Lib/__pycache__', + u'build', + u'Doc/build'], + 'filter': '*', + 'include_hidden': False, + 'recursive': True, + 'watch_for_changes': True}] +proj.file-type = 'shared' diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 71b14373943..4c96204158a 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -4953,13 +4953,8 @@ posix_utime(PyObject *self, PyObject *args, PyObject *kwargs) } if (utime.now) { - SYSTEMTIME now; - GetSystemTime(&now); - if (!SystemTimeToFileTime(&now, &mtime) || - !SystemTimeToFileTime(&now, &atime)) { - PyErr_SetFromWindowsErr(0); - goto exit; - } + GetSystemTimeAsFileTime(&mtime); + atime = mtime; } else { time_t_to_FILE_TIME(utime.atime_s, utime.atime_ns, &atime); diff --git a/Modules/timemodule.c b/Modules/timemodule.c index ca399069948..d3878b95d99 100644 --- a/Modules/timemodule.c +++ b/Modules/timemodule.c @@ -650,7 +650,7 @@ time_strftime(PyObject *self, PyObject *args) return NULL; } } -#elif defined(_AIX) && defined(HAVE_WCSFTIME) +#elif (defined(_AIX) || defined(sun)) && defined(HAVE_WCSFTIME) for(outbuf = wcschr(fmt, '%'); outbuf != NULL; outbuf = wcschr(outbuf+2, '%'))