From 42205ee512159de62c01e202ff799d78fac9ac26 Mon Sep 17 00:00:00 2001 From: Ned Deily Date: Tue, 20 Jul 2021 13:09:30 -0400 Subject: [PATCH 1/5] Update macOS build-installer script comments for 3.10 and remove unused pre-10.5 vestiges. (GH-27253) --- Mac/BuildScript/README.rst | 84 ++++++++++++++++++++++++------ Mac/BuildScript/build-installer.py | 49 ++--------------- 2 files changed, 74 insertions(+), 59 deletions(-) diff --git a/Mac/BuildScript/README.rst b/Mac/BuildScript/README.rst index 94a6bb28cadfa5..4f74e7dc00520a 100644 --- a/Mac/BuildScript/README.rst +++ b/Mac/BuildScript/README.rst @@ -19,33 +19,87 @@ Starting with macOS 10.15 Catalina, Gatekeeper now also requires that installer packages are submitted to and pass Apple's automated notarization service using the altool command. To pass notarization, the binaries included in the package must be built with at least -the macOS 10.9 SDK, mout now be signed with the codesign utility +the macOS 10.9 SDK, must now be signed with the codesign utility, and executables must opt in to the hardened run time option with any necessary entitlements. Details of these processes are available in the on-line Apple Developer Documentation and man pages. -As of 3.8.0 and 3.7.7, PSF practice is to build one installer variants -for each release. Note that as of this writing, no Pythons support -building on a newer version of macOS that will run on older versions +A goal of PSF-provided (python.org) Python binaries for macOS is to +support a wide-range of operating system releases with one set of +binaries. Currently, the oldest release supported by python.org +binaries is macOS 10.9; it is still possible to build Python and +Python installers on older versions of macOS but we not regularly +test on those systems nor provide binaries for them. + +Prior to Python 3.9.1, no Python releases supported building on a +newer version of macOS that will run on older versions by setting MACOSX_DEPLOYMENT_TARGET. This is because the various -Python C modules do not yet support runtime testing of macOS +Python C modules did not yet support runtime testing of macOS feature availability (for example, by using macOS AvailabilityMacros.h -and weak-linking). To build a Python that is to be used on a -range of macOS releases, always build on the oldest release to be -supported; the necessary shared libraries for that release will -normally also be available on later systems, with the occasional -exception such as the removal of 32-bit libraries in macOS 10.15. - -build-installer requires Apple Developer tools, either from the +and weak-linking). To build a Python that is to be used on a +range of macOS releases, it was necessary to always build on the +oldest release to be supported; the necessary shared libraries for +that release will normally also be available on later systems, +with the occasional exception such as the removal of 32-bit +libraries in macOS 10.15. For 3.9.x and recent earlier systems, +PSF practice was to provide a "macOS 64-bit Intel installer" variant +that was built on 10.9 that would run on macOS 10.9 and later. + +Starting with 3.9.1, Python fully supports macOS "weaklinking", +meaning it is now possible to build a Python on a current macOS version +with a deployment target of an earlier macOS system. For 3.9.1 and +later systems, we provide a "macOS 64-bit universal2 installer" +variant, currently build on macOS 11 Big Sur with fat binaries +natively supporting both Apple Silicon (arm64) and Intel-64 +(x86_64) Macs running macOS 10.9 or later. + +The legacy "macOS 64-bit Intel installer" variant is expected to +be retired prior to the end of 3.9.x support. + +build-installer.py requires Apple Developer tools, either from the Command Line Tools package or from a full Xcode installation. You should use the most recent version of either for the operating system version in use. (One notable exception: on macOS 10.6, Snow Leopard, use Xcode 3, not Xcode 4 which was released later -in the 10.6 support cycle.) +in the 10.6 support cycle.) build-installer.py also must be run +with recent versions of Python 3.x or 2.7. On older systems, +due to changes in TLS practices, it may be easier to manually +download and cache third-party source distributions used by +build-installer.py rather than have it attempt to automatically +download them. + +1. universal2, arm64 and x86_64, for OS X 10.9 (and later):: + + /path/to/bootstrap/python3 build-installer.py \ + --universal-archs=universal2 \ + --dep-target=10.9 + + - builds the following third-party libraries + + * OpenSSL 1.1.1 + * Tcl/Tk 8.6 + * NCurses + * SQLite + * XZ + * libffi + + - uses system-supplied versions of third-party libraries + + * readline module links with Apple BSD editline (libedit) + * zlib + * bz2 + + - recommended build environment: + + * Mac OS X 11 or later + * Xcode Command Line Tools 12.5 or later + * current default macOS SDK + * ``MACOSX_DEPLOYMENT_TARGET=10.9`` + * Apple ``clang`` -1. 64-bit, x86_64, for OS X 10.9 (and later):: +2. legacy Intel 64-bit, x86_64, for OS X 10.9 (and later):: - /path/to/bootstrap/python2.7 build-installer.py \ + /path/to/bootstrap/python3 build-installer.py \ --universal-archs=intel-64 \ --dep-target=10.9 diff --git a/Mac/BuildScript/build-installer.py b/Mac/BuildScript/build-installer.py index 60174eb6962bae..56d3d4ba380edd 100755 --- a/Mac/BuildScript/build-installer.py +++ b/Mac/BuildScript/build-installer.py @@ -2,6 +2,10 @@ """ This script is used to build "official" universal installers on macOS. +NEW for 3.10 and backports: +- support universal2 variant with arm64 and x86_64 archs +- enable clang optimizations when building on 10.15+ + NEW for 3.9.0 and backports: - 2.7 end-of-life issues: - Python 3 installs now update the Current version link @@ -236,8 +240,6 @@ def tweak_tcl_build(basedir, archList): def library_recipes(): result = [] - LT_10_5 = bool(getDeptargetTuple() < (10, 5)) - # Since Apple removed the header files for the deprecated system # OpenSSL as of the Xcode 7 release (for OS X 10.10+), we do not # have much choice but to build our own copy here, too. @@ -367,7 +369,7 @@ def library_recipes(): '-DSQLITE_ENABLE_RTREE ' '-DSQLITE_OMIT_AUTOINIT ' '-DSQLITE_TCL=0 ' - '%s' % ('','-DSQLITE_WITHOUT_ZONEMALLOC ')[LT_10_5]), + ), configure_pre=[ '--enable-threadsafe', '--enable-shared=no', @@ -378,47 +380,6 @@ def library_recipes(): ), ]) - if getDeptargetTuple() < (10, 5): - result.extend([ - dict( - name="Bzip2 1.0.6", - url="http://bzip.org/1.0.6/bzip2-1.0.6.tar.gz", - checksum='00b516f4704d4a7cb50a1d97e6e8e15b', - configure=None, - install='make install CC=%s CXX=%s, PREFIX=%s/usr/local/ CFLAGS="-arch %s"'%( - CC, CXX, - shellQuote(os.path.join(WORKDIR, 'libraries')), - ' -arch '.join(ARCHLIST), - ), - ), - dict( - name="ZLib 1.2.3", - url="http://www.gzip.org/zlib/zlib-1.2.3.tar.gz", - checksum='debc62758716a169df9f62e6ab2bc634', - configure=None, - install='make install CC=%s CXX=%s, prefix=%s/usr/local/ CFLAGS="-arch %s"'%( - CC, CXX, - shellQuote(os.path.join(WORKDIR, 'libraries')), - ' -arch '.join(ARCHLIST), - ), - ), - dict( - # Note that GNU readline is GPL'd software - name="GNU Readline 6.1.2", - url="http://ftp.gnu.org/pub/gnu/readline/readline-6.1.tar.gz" , - checksum='fc2f7e714fe792db1ce6ddc4c9fb4ef3', - patchlevel='0', - patches=[ - # The readline maintainers don't do actual micro releases, but - # just ship a set of patches. - ('http://ftp.gnu.org/pub/gnu/readline/readline-6.1-patches/readline61-001', - 'c642f2e84d820884b0bf9fd176bc6c3f'), - ('http://ftp.gnu.org/pub/gnu/readline/readline-6.1-patches/readline61-002', - '1a76781a1ea734e831588285db7ec9b1'), - ] - ), - ]) - if not PYTHON_3: result.extend([ dict( From 85fa3b6b7c11897732fedc443db0e4e8e380c8f8 Mon Sep 17 00:00:00 2001 From: Leonardo Freua Date: Tue, 20 Jul 2021 14:15:45 -0300 Subject: [PATCH 2/5] bpo-44631: Make the repr() of the _Environ class more readable. (#27128) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Łukasz Langa --- Lib/os.py | 8 +++++--- Lib/test/test_os.py | 8 +++++--- .../2021-07-13-22-25-13.bpo-44631.qkGwe4.rst | 1 + 3 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Documentation/2021-07-13-22-25-13.bpo-44631.qkGwe4.rst diff --git a/Lib/os.py b/Lib/os.py index d26cfc99939f39..8cc70a11e9bc89 100644 --- a/Lib/os.py +++ b/Lib/os.py @@ -704,9 +704,11 @@ def __len__(self): return len(self._data) def __repr__(self): - return 'environ({{{}}})'.format(', '.join( - ('{!r}: {!r}'.format(self.decodekey(key), self.decodevalue(value)) - for key, value in self._data.items()))) + formatted_items = ", ".join( + f"{self.decodekey(key)!r}: {self.decodevalue(value)!r}" + for key, value in self._data.items() + ) + return f"environ({{{formatted_items}}})" def copy(self): return dict(self) diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index 684e308ad3a059..00e738ecf9a1c3 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -1029,9 +1029,11 @@ def test_items(self): def test___repr__(self): """Check that the repr() of os.environ looks like environ({...}).""" env = os.environ - self.assertEqual(repr(env), 'environ({{{}}})'.format(', '.join( - '{!r}: {!r}'.format(key, value) - for key, value in env.items()))) + formatted_items = ", ".join( + f"{key!r}: {value!r}" + for key, value in env.items() + ) + self.assertEqual(repr(env), f"environ({{{formatted_items}}})") def test_get_exec_path(self): defpath_list = os.defpath.split(os.pathsep) diff --git a/Misc/NEWS.d/next/Documentation/2021-07-13-22-25-13.bpo-44631.qkGwe4.rst b/Misc/NEWS.d/next/Documentation/2021-07-13-22-25-13.bpo-44631.qkGwe4.rst new file mode 100644 index 00000000000000..b0898fe1ad9995 --- /dev/null +++ b/Misc/NEWS.d/next/Documentation/2021-07-13-22-25-13.bpo-44631.qkGwe4.rst @@ -0,0 +1 @@ +Refactored the ``repr()`` code of the ``_Environ`` (os module). \ No newline at end of file From 7f1c330da31c54e028dceaf3610877914c2a4497 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 20 Jul 2021 19:15:07 +0100 Subject: [PATCH 3/5] bpo-44566: resolve differences between asynccontextmanager and contextmanager (#27024) --- Lib/contextlib.py | 104 ++++++++++-------- Lib/test/test_contextlib.py | 23 ++-- Lib/test/test_contextlib_async.py | 13 ++- .../2021-07-05-18-13-25.bpo-44566.o51Bd1.rst | 1 + 4 files changed, 85 insertions(+), 56 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2021-07-05-18-13-25.bpo-44566.o51Bd1.rst diff --git a/Lib/contextlib.py b/Lib/contextlib.py index 004d1037b78a47..8343d7e5196713 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -113,18 +113,20 @@ def __init__(self, func, args, kwds): # for the class instead. # See http://bugs.python.org/issue19404 for more details. - -class _GeneratorContextManager(_GeneratorContextManagerBase, - AbstractContextManager, - ContextDecorator): - """Helper for @contextmanager decorator.""" - def _recreate_cm(self): - # _GCM instances are one-shot context managers, so the + # _GCMB instances are one-shot context managers, so the # CM must be recreated each time a decorated function is # called return self.__class__(self.func, self.args, self.kwds) + +class _GeneratorContextManager( + _GeneratorContextManagerBase, + AbstractContextManager, + ContextDecorator, +): + """Helper for @contextmanager decorator.""" + def __enter__(self): # do not keep args and kwds alive unnecessarily # they are only needed for recreation, which is not possible anymore @@ -134,8 +136,8 @@ def __enter__(self): except StopIteration: raise RuntimeError("generator didn't yield") from None - def __exit__(self, type, value, traceback): - if type is None: + def __exit__(self, typ, value, traceback): + if typ is None: try: next(self.gen) except StopIteration: @@ -146,9 +148,9 @@ def __exit__(self, type, value, traceback): if value is None: # Need to force instantiation so we can reliably # tell if we get the same exception back - value = type() + value = typ() try: - self.gen.throw(type, value, traceback) + self.gen.throw(typ, value, traceback) except StopIteration as exc: # Suppress StopIteration *unless* it's the same exception that # was passed to throw(). This prevents a StopIteration @@ -158,81 +160,93 @@ def __exit__(self, type, value, traceback): # Don't re-raise the passed in exception. (issue27122) if exc is value: return False - # Likewise, avoid suppressing if a StopIteration exception + # Avoid suppressing if a StopIteration exception # was passed to throw() and later wrapped into a RuntimeError - # (see PEP 479). - if type is StopIteration and exc.__cause__ is value: + # (see PEP 479 for sync generators; async generators also + # have this behavior). But do this only if the exception wrapped + # by the RuntimeError is actually Stop(Async)Iteration (see + # issue29692). + if ( + isinstance(value, StopIteration) + and exc.__cause__ is value + ): return False raise - except: + except BaseException as exc: # only re-raise if it's *not* the exception that was # passed to throw(), because __exit__() must not raise # an exception unless __exit__() itself failed. But throw() # has to raise the exception to signal propagation, so this # fixes the impedance mismatch between the throw() protocol # and the __exit__() protocol. - # - # This cannot use 'except BaseException as exc' (as in the - # async implementation) to maintain compatibility with - # Python 2, where old-style class exceptions are not caught - # by 'except BaseException'. - if sys.exc_info()[1] is value: - return False - raise + if exc is not value: + raise + return False raise RuntimeError("generator didn't stop after throw()") - -class _AsyncGeneratorContextManager(_GeneratorContextManagerBase, - AbstractAsyncContextManager, - AsyncContextDecorator): - """Helper for @asynccontextmanager.""" - - def _recreate_cm(self): - # _AGCM instances are one-shot context managers, so the - # ACM must be recreated each time a decorated function is - # called - return self.__class__(self.func, self.args, self.kwds) +class _AsyncGeneratorContextManager( + _GeneratorContextManagerBase, + AbstractAsyncContextManager, + AsyncContextDecorator, +): + """Helper for @asynccontextmanager decorator.""" async def __aenter__(self): + # do not keep args and kwds alive unnecessarily + # they are only needed for recreation, which is not possible anymore + del self.args, self.kwds, self.func try: - return await self.gen.__anext__() + return await anext(self.gen) except StopAsyncIteration: raise RuntimeError("generator didn't yield") from None async def __aexit__(self, typ, value, traceback): if typ is None: try: - await self.gen.__anext__() + await anext(self.gen) except StopAsyncIteration: - return + return False else: raise RuntimeError("generator didn't stop") else: if value is None: + # Need to force instantiation so we can reliably + # tell if we get the same exception back value = typ() - # See _GeneratorContextManager.__exit__ for comments on subtleties - # in this implementation try: await self.gen.athrow(typ, value, traceback) - raise RuntimeError("generator didn't stop after athrow()") except StopAsyncIteration as exc: + # Suppress StopIteration *unless* it's the same exception that + # was passed to throw(). This prevents a StopIteration + # raised inside the "with" statement from being suppressed. return exc is not value except RuntimeError as exc: + # Don't re-raise the passed in exception. (issue27122) if exc is value: return False - # Avoid suppressing if a StopIteration exception - # was passed to throw() and later wrapped into a RuntimeError + # Avoid suppressing if a Stop(Async)Iteration exception + # was passed to athrow() and later wrapped into a RuntimeError # (see PEP 479 for sync generators; async generators also # have this behavior). But do this only if the exception wrapped # by the RuntimeError is actually Stop(Async)Iteration (see # issue29692). - if isinstance(value, (StopIteration, StopAsyncIteration)): - if exc.__cause__ is value: - return False + if ( + isinstance(value, (StopIteration, StopAsyncIteration)) + and exc.__cause__ is value + ): + return False raise except BaseException as exc: + # only re-raise if it's *not* the exception that was + # passed to throw(), because __exit__() must not raise + # an exception unless __exit__() itself failed. But throw() + # has to raise the exception to signal propagation, so this + # fixes the impedance mismatch between the throw() protocol + # and the __exit__() protocol. if exc is not value: raise + return False + raise RuntimeError("generator didn't stop after athrow()") def contextmanager(func): diff --git a/Lib/test/test_contextlib.py b/Lib/test/test_contextlib.py index 9c27866cd661cb..04720d949fee5b 100644 --- a/Lib/test/test_contextlib.py +++ b/Lib/test/test_contextlib.py @@ -126,19 +126,22 @@ def woohoo(): self.assertEqual(state, [1, 42, 999]) def test_contextmanager_except_stopiter(self): - stop_exc = StopIteration('spam') @contextmanager def woohoo(): yield - try: - with self.assertWarnsRegex(DeprecationWarning, - "StopIteration"): - with woohoo(): - raise stop_exc - except Exception as ex: - self.assertIs(ex, stop_exc) - else: - self.fail('StopIteration was suppressed') + + class StopIterationSubclass(StopIteration): + pass + + for stop_exc in (StopIteration('spam'), StopIterationSubclass('spam')): + with self.subTest(type=type(stop_exc)): + try: + with woohoo(): + raise stop_exc + except Exception as ex: + self.assertIs(ex, stop_exc) + else: + self.fail(f'{stop_exc} was suppressed') def test_contextmanager_except_pep479(self): code = """\ diff --git a/Lib/test/test_contextlib_async.py b/Lib/test/test_contextlib_async.py index 7904abff7d1aa5..6a218f911569c9 100644 --- a/Lib/test/test_contextlib_async.py +++ b/Lib/test/test_contextlib_async.py @@ -209,7 +209,18 @@ async def test_contextmanager_except_stopiter(self): async def woohoo(): yield - for stop_exc in (StopIteration('spam'), StopAsyncIteration('ham')): + class StopIterationSubclass(StopIteration): + pass + + class StopAsyncIterationSubclass(StopAsyncIteration): + pass + + for stop_exc in ( + StopIteration('spam'), + StopAsyncIteration('ham'), + StopIterationSubclass('spam'), + StopAsyncIterationSubclass('spam') + ): with self.subTest(type=type(stop_exc)): try: async with woohoo(): diff --git a/Misc/NEWS.d/next/Library/2021-07-05-18-13-25.bpo-44566.o51Bd1.rst b/Misc/NEWS.d/next/Library/2021-07-05-18-13-25.bpo-44566.o51Bd1.rst new file mode 100644 index 00000000000000..3b00a1b715feef --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-07-05-18-13-25.bpo-44566.o51Bd1.rst @@ -0,0 +1 @@ +handle StopIteration subclass raised from @contextlib.contextmanager generator \ No newline at end of file From 6564656495d456a1bcc1aaa06abfc696209f37b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Kul=C3=ADk?= Date: Tue, 20 Jul 2021 20:16:23 +0200 Subject: [PATCH 4/5] bpo-43219: skip Solaris in the test as well (GH-27257) --- Lib/test/test_shutil.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index e2574253d961f9..7bf60fd566e13f 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -37,6 +37,7 @@ TESTFN_SRC = TESTFN + "_SRC" TESTFN_DST = TESTFN + "_DST" MACOS = sys.platform.startswith("darwin") +SOLARIS = sys.platform.startswith("sunos") AIX = sys.platform[:3] == 'aix' try: import grp @@ -1249,7 +1250,7 @@ def test_copyfile_same_file(self): # Make sure file is not corrupted. self.assertEqual(read_file(src_file), 'foo') - @unittest.skipIf(MACOS or _winapi, 'On MACOS and Windows the errors are not confusing (though different)') + @unittest.skipIf(MACOS or SOLARIS or _winapi, 'On MACOS, Solaris and Windows the errors are not confusing (though different)') def test_copyfile_nonexistent_dir(self): # Issue 43219 src_dir = self.mkdtemp() From 3b56b3b97d91e2b412ce1b2bcaddcd43ef3d223b Mon Sep 17 00:00:00 2001 From: Mohamad Mansour <66031317+mohamadmansourX@users.noreply.github.com> Date: Tue, 20 Jul 2021 21:56:57 +0300 Subject: [PATCH 5/5] bpo-44539: Support recognizing JPEG files without JFIF or Exif markers (GH-26964) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: moemansour03@gmail.com Co-authored-by: Éric Araujo Co-authored-by: Łukasz Langa --- Lib/imghdr.py | 4 +++- Lib/test/imghdrdata/python-raw.jpg | Bin 0 -> 525 bytes Lib/test/test_imghdr.py | 1 + .../2021-06-30-11-34-35.bpo-44539.nP0Xi4.rst | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 Lib/test/imghdrdata/python-raw.jpg create mode 100644 Misc/NEWS.d/next/Library/2021-06-30-11-34-35.bpo-44539.nP0Xi4.rst diff --git a/Lib/imghdr.py b/Lib/imghdr.py index 6e01fd857469ad..afcb67772ee9a9 100644 --- a/Lib/imghdr.py +++ b/Lib/imghdr.py @@ -35,9 +35,11 @@ def what(file, h=None): tests = [] def test_jpeg(h, f): - """JPEG data in JFIF or Exif format""" + """JPEG data with JFIF or Exif markers; and raw JPEG""" if h[6:10] in (b'JFIF', b'Exif'): return 'jpeg' + elif h[:4] == b'\xff\xd8\xff\xdb': + return 'jpeg' tests.append(test_jpeg) diff --git a/Lib/test/imghdrdata/python-raw.jpg b/Lib/test/imghdrdata/python-raw.jpg new file mode 100644 index 0000000000000000000000000000000000000000..11940b3410ddf052a996d705006236172e153c25 GIT binary patch literal 525 zcmex=y%&ai23_z|RGYc!5B7=~js90j-!~eG!c$gW1&R`Z~uxAiib}37`_~0{Pp(|5m zJ_{y4QBB>qE9ySbOP zKJvD_{#YsalT_xtD}`PbTbf>YcWM=8%AM6!wEp;hr&P(BcMGFR`>uMdNsVspZfvbq z)SQvNGJDES&s6`NCt5WHy&JVYRNee!(@+<>kM~>BLWeUip3Sn2+3vjgdR1z~#+!kP z9s5&uT>f}CgGKYeg@3zkm>$@Kuy5IWvpVFN`tQ8^QxZg)l7xGA1kKPeXYDMiOn73{ ZnSZ#cZ+TG9D}~;D6_WDoYwQ2t1OUankdy!b literal 0 HcmV?d00001 diff --git a/Lib/test/test_imghdr.py b/Lib/test/test_imghdr.py index b2d1fc8322a038..ca0a0b23c3cf1a 100644 --- a/Lib/test/test_imghdr.py +++ b/Lib/test/test_imghdr.py @@ -16,6 +16,7 @@ ('python.pgm', 'pgm'), ('python.pbm', 'pbm'), ('python.jpg', 'jpeg'), + ('python-raw.jpg', 'jpeg'), # raw JPEG without JFIF/EXIF markers ('python.ras', 'rast'), ('python.sgi', 'rgb'), ('python.tiff', 'tiff'), diff --git a/Misc/NEWS.d/next/Library/2021-06-30-11-34-35.bpo-44539.nP0Xi4.rst b/Misc/NEWS.d/next/Library/2021-06-30-11-34-35.bpo-44539.nP0Xi4.rst new file mode 100644 index 00000000000000..f5e831afce8358 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-06-30-11-34-35.bpo-44539.nP0Xi4.rst @@ -0,0 +1 @@ +Added support for recognizing JPEG files without JFIF or Exif markers.