From a8f0c9c995af33d746fa8b33df44e92b055219cd Mon Sep 17 00:00:00 2001 From: grantlouisherman Date: Wed, 20 May 2026 00:17:31 -0400 Subject: [PATCH 01/15] bugfix(150107): adding isinstance check on offset to include if available Signed-off-by: grantlouisherman --- Lib/asyncio/base_events.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 1fedb066f94c53..344b5d8b4e03ac 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -969,7 +969,7 @@ async def _sock_sendfile_native(self, sock, file, offset, count): f"and file {file!r} combination") async def _sock_sendfile_fallback(self, sock, file, offset, count): - if offset: + if isinstance(offset, int): file.seek(offset) blocksize = ( min(count, constants.SENDFILE_FALLBACK_READBUFFER_SIZE) @@ -1286,7 +1286,6 @@ async def sendfile(self, transport, file, offset=0, count=None, raise RuntimeError( f"fallback is disabled and native sendfile is not " f"supported for transport {transport!r}") - return await self._sendfile_fallback(transport, file, offset, count) @@ -1295,7 +1294,7 @@ async def _sendfile_native(self, transp, file, offset, count): "sendfile syscall is not supported") async def _sendfile_fallback(self, transp, file, offset, count): - if offset: + if isinstance(offset, int): file.seek(offset) blocksize = min(count, 16384) if count else 16384 buf = bytearray(blocksize) From d2a9ce0dd026e53635dfc28aaf0a74c987574848 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 14:17:31 +0000 Subject: [PATCH 02/15] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-05-21-14-17-29.gh-issue-150107.hwPKW6.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-05-21-14-17-29.gh-issue-150107.hwPKW6.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-21-14-17-29.gh-issue-150107.hwPKW6.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-21-14-17-29.gh-issue-150107.hwPKW6.rst new file mode 100644 index 00000000000000..8bcd83407d2ee9 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-21-14-17-29.gh-issue-150107.hwPKW6.rst @@ -0,0 +1 @@ +Offset was not being honored in asyncio because conditional was checking if offset was truthy or falsey. However, if offset is 0 then it is deemed falsey even though 0 is acceptable number to file.seek to. I changed the conditional to use instance of ints to validate if the program should file.seek or not From b525cb2c9b6cb75e567bfad905dea820f04c447f Mon Sep 17 00:00:00 2001 From: grantlouisherman Date: Thu, 21 May 2026 11:57:59 -0400 Subject: [PATCH 03/15] addressing PR comments Signed-off-by: grantlouisherman --- Lib/asyncio/base_events.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 344b5d8b4e03ac..3732294c5848f0 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -969,7 +969,7 @@ async def _sock_sendfile_native(self, sock, file, offset, count): f"and file {file!r} combination") async def _sock_sendfile_fallback(self, sock, file, offset, count): - if isinstance(offset, int): + if hasattr(file, 'seek'): file.seek(offset) blocksize = ( min(count, constants.SENDFILE_FALLBACK_READBUFFER_SIZE) @@ -1294,7 +1294,7 @@ async def _sendfile_native(self, transp, file, offset, count): "sendfile syscall is not supported") async def _sendfile_fallback(self, transp, file, offset, count): - if isinstance(offset, int): + if hasattr(file, 'seek'): file.seek(offset) blocksize = min(count, 16384) if count else 16384 buf = bytearray(blocksize) From f247be00a639854a599d2465c198fd05af9fe52e Mon Sep 17 00:00:00 2001 From: grantlouisherman Date: Thu, 21 May 2026 12:01:13 -0400 Subject: [PATCH 04/15] addressing PR comments Signed-off-by: grantlouisherman --- .../2026-05-21-14-17-29.gh-issue-150107.hwPKW6.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-21-14-17-29.gh-issue-150107.hwPKW6.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-21-14-17-29.gh-issue-150107.hwPKW6.rst index 8bcd83407d2ee9..76f83630d37cd7 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-21-14-17-29.gh-issue-150107.hwPKW6.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-21-14-17-29.gh-issue-150107.hwPKW6.rst @@ -1 +1,3 @@ -Offset was not being honored in asyncio because conditional was checking if offset was truthy or falsey. However, if offset is 0 then it is deemed falsey even though 0 is acceptable number to file.seek to. I changed the conditional to use instance of ints to validate if the program should file.seek or not +:mod:`asyncio`: ``sendfile()`` and ``sock_sendfile()`` event loop methods +now call ``file.seek(offset)`` if *file* has a `seek()` method, +even if *offset* is `0` (default value)`. From 84cdc6c41fffb7e9382ecc71a0d728b8ef3fffed Mon Sep 17 00:00:00 2001 From: grantlouisherman Date: Thu, 21 May 2026 12:12:02 -0400 Subject: [PATCH 05/15] fixing lints Signed-off-by: grantlouisherman --- .../2026-05-21-14-17-29.gh-issue-150107.hwPKW6.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-21-14-17-29.gh-issue-150107.hwPKW6.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-21-14-17-29.gh-issue-150107.hwPKW6.rst index 76f83630d37cd7..a13f249e48cc02 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-21-14-17-29.gh-issue-150107.hwPKW6.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-21-14-17-29.gh-issue-150107.hwPKW6.rst @@ -1,3 +1,3 @@ :mod:`asyncio`: ``sendfile()`` and ``sock_sendfile()`` event loop methods -now call ``file.seek(offset)`` if *file* has a `seek()` method, -even if *offset* is `0` (default value)`. +now call ``file.seek(offset)`` if *file* has a ``seek()`` method, +even if *offset* is ``0`` (default value). From 801932f11e159b1dca6291c3dafceb980516114c Mon Sep 17 00:00:00 2001 From: grantlouisherman Date: Thu, 21 May 2026 12:56:04 -0400 Subject: [PATCH 06/15] adding unit test Signed-off-by: grantlouisherman --- Lib/test/test_asyncio/test_sendfile.py | 28 +++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_asyncio/test_sendfile.py b/Lib/test/test_asyncio/test_sendfile.py index dcd963b3355ef8..efab4b84bc8ee8 100644 --- a/Lib/test/test_asyncio/test_sendfile.py +++ b/Lib/test/test_asyncio/test_sendfile.py @@ -227,7 +227,34 @@ def test_sock_sendfile_zero_size(self): self.assertEqual(ret, 0) self.assertEqual(self.file.tell(), 0) + def check_sock_sendfile_offset(self, data, offset, force_fallback=False): + sock, proto = self.prepare_socksendfile() + with tempfile.TemporaryFile() as f: + f.write(data) + f.flush() + self.assertEqual(f.tell(), len(data)) + + if force_fallback: + async def _sock_sendfile_fail(sock, file, offset, count): + raise asyncio.exceptions.SendfileNotAvailableError() + with support.swap_attr(self.loop, '_sock_sendfile_native', _sock_sendfile_fail): + ret = self.run_loop(self.loop.sock_sendfile(sock, f, offset, None)) + else: + ret = self.run_loop(self.loop.sock_sendfile(sock, f, offset, None)) + self.assertEqual(f.tell(), len(data)) + + sock.close() + self.run_loop(proto.wait_closed()) + + self.assertEqual(ret, len(data) - offset) + + def test_sock_sendfile_offset(self): + data = b'abcdef' + for offset in (0, len(data) // 2, len(data)): + for force_fallback in (False, True): + with self.subTest(offset=offset, force_fallback=force_fallback): + self.check_sock_sendfile_offset(data, offset, force_fallback) def test_sock_sendfile_mix_with_regular_send(self): buf = b"mix_regular_send" * (4 * 1024) # 64 KiB sock, proto = self.prepare_socksendfile() @@ -450,7 +477,6 @@ def test_sendfile_ssl_close_peer_after_receiving(self): self.assertEqual(ret, len(self.DATA)) self.assertEqual(srv_proto.nbytes, len(self.DATA)) self.assertEqual(srv_proto.data, self.DATA) - self.assertEqual(self.file.tell(), len(self.DATA)) # On Solaris, lowering SO_RCVBUF on a TCP connection after it has been # established has no effect. Due to its age, this bug affects both Oracle From 800a2d51a995e9c2fe2a0ea66984720845abd7b9 Mon Sep 17 00:00:00 2001 From: grantlouisherman Date: Thu, 21 May 2026 13:48:44 -0400 Subject: [PATCH 07/15] fixing test failures and lint errors Signed-off-by: grantlouisherman --- Lib/test/test_asyncio/test_sendfile.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_asyncio/test_sendfile.py b/Lib/test/test_asyncio/test_sendfile.py index efab4b84bc8ee8..ae7a2e32c52687 100644 --- a/Lib/test/test_asyncio/test_sendfile.py +++ b/Lib/test/test_asyncio/test_sendfile.py @@ -227,6 +227,7 @@ def test_sock_sendfile_zero_size(self): self.assertEqual(ret, 0) self.assertEqual(self.file.tell(), 0) + def check_sock_sendfile_offset(self, data, offset, force_fallback=False): sock, proto = self.prepare_socksendfile() with tempfile.TemporaryFile() as f: @@ -234,20 +235,17 @@ def check_sock_sendfile_offset(self, data, offset, force_fallback=False): f.flush() self.assertEqual(f.tell(), len(data)) - if force_fallback: - async def _sock_sendfile_fail(sock, file, offset, count): - raise asyncio.exceptions.SendfileNotAvailableError() - with support.swap_attr(self.loop, '_sock_sendfile_native', _sock_sendfile_fail): + if force_fallback: + async def _sock_sendfile_fail(sock, file, offset, count): + raise asyncio.exceptions.SendfileNotAvailableError() + with support.swap_attr(self.loop, '_sock_sendfile_native', _sock_sendfile_fail): + ret = self.run_loop(self.loop.sock_sendfile(sock, f, offset, None)) + else: ret = self.run_loop(self.loop.sock_sendfile(sock, f, offset, None)) - else: - ret = self.run_loop(self.loop.sock_sendfile(sock, f, offset, None)) - - self.assertEqual(f.tell(), len(data)) - - sock.close() - self.run_loop(proto.wait_closed()) - - self.assertEqual(ret, len(data) - offset) + self.assertEqual(f.tell(), len(data)) + sock.close() + self.run_loop(proto.wait_closed()) + self.assertEqual(ret, len(data) - offset) def test_sock_sendfile_offset(self): data = b'abcdef' @@ -255,6 +253,7 @@ def test_sock_sendfile_offset(self): for force_fallback in (False, True): with self.subTest(offset=offset, force_fallback=force_fallback): self.check_sock_sendfile_offset(data, offset, force_fallback) + def test_sock_sendfile_mix_with_regular_send(self): buf = b"mix_regular_send" * (4 * 1024) # 64 KiB sock, proto = self.prepare_socksendfile() @@ -477,6 +476,7 @@ def test_sendfile_ssl_close_peer_after_receiving(self): self.assertEqual(ret, len(self.DATA)) self.assertEqual(srv_proto.nbytes, len(self.DATA)) self.assertEqual(srv_proto.data, self.DATA) + self.assertEqual(self.file.tell(), len(self.DATA)) # On Solaris, lowering SO_RCVBUF on a TCP connection after it has been # established has no effect. Due to its age, this bug affects both Oracle From 2af301529773c7d782b88a4c901c006bd49dd774 Mon Sep 17 00:00:00 2001 From: grantlouisherman Date: Thu, 21 May 2026 16:22:34 -0400 Subject: [PATCH 08/15] fixing windows tests Signed-off-by: grantlouisherman --- Lib/asyncio/windows_events.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/asyncio/windows_events.py b/Lib/asyncio/windows_events.py index 5f75b17d8ca649..0bf7732136f1f8 100644 --- a/Lib/asyncio/windows_events.py +++ b/Lib/asyncio/windows_events.py @@ -610,6 +610,9 @@ def sendfile(self, sock, file, offset, count): ov = _overlapped.Overlapped(NULL) offset_low = offset & 0xffff_ffff offset_high = (offset >> 32) & 0xffff_ffff + # TransmitFile ignores OVERLAPPED.Offset for handles not opened with + # FILE_FLAG_OVERLAPPED, so seek the CRT file pointer to match. + file.seek(offset) ov.TransmitFile(sock.fileno(), msvcrt.get_osfhandle(file.fileno()), offset_low, offset_high, From aaa1a2ddef44d4dfe3e3b12334c08b2625d1217a Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 21 May 2026 21:06:42 -0700 Subject: [PATCH 09/15] gh-149995: Update typing.py docstrings and documentation (#149996) Some of these docstrings read as if they were written when typing.py was first written, and things have evolved since then. A few motivations: - Call protocols protocols instead of ABCs. They are also ABCs, but the fact they are protocols is more relevant to typing. - Avoid recommending direct use of .__annotations__ and steer users to annotationlib instead. - For TypedDict, mention NotRequired before total=False since it is more general and probably more frequently useful. - For overloads, mention runtime use first instead of stub use. I think early on there was talk of allowing overload only in stubs, but it is now heavily used at runtime too and that's more likely to be relevant to users. --- Doc/library/typing.rst | 46 +++---- Lib/typing.py | 115 +++++++++--------- ...-05-18-07-44-46.gh-issue-149995.vvtFHn.rst | 1 + 3 files changed, 81 insertions(+), 81 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-05-18-07-44-46.gh-issue-149995.vvtFHn.rst diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 71b395c80166cc..b2167cbc63a1ff 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -719,8 +719,8 @@ The :data:`Any` type ==================== A special kind of type is :data:`Any`. A static type checker will treat -every type as being compatible with :data:`Any` and :data:`Any` as being -compatible with every type. +every type as assignable to :data:`Any` and :data:`Any` as assignable to +every type. This means that it is possible to perform any operation or method call on a value of type :data:`Any` and assign it to any variable:: @@ -785,7 +785,7 @@ it as a return value) of a more specialized type is a type error. For example:: hash_a(42) hash_a("foo") - # Passes type checking, since Any is compatible with all types + # Passes type checking, since Any is assignable to all types hash_b(42) hash_b("foo") @@ -851,8 +851,8 @@ using ``[]``. Special type indicating an unconstrained type. - * Every type is compatible with :data:`Any`. - * :data:`Any` is compatible with every type. + * Every type is assignable to :data:`Any`. + * :data:`Any` is assignable to every type. .. versionchanged:: 3.11 :data:`Any` can now be used as a base class. This can be useful for @@ -1292,10 +1292,10 @@ These can be used as types in annotations. They all support subscription using :data:`ClassVar` accepts only types and cannot be further subscribed. - :data:`ClassVar` is not a class itself, and should not + :data:`ClassVar` is not a class itself, and cannot be used with :func:`isinstance` or :func:`issubclass`. :data:`ClassVar` does not change Python runtime behavior, but - it can be used by third-party type checkers. For example, a type checker + it can be used by static type checkers. For example, a type checker might flag the following code as an error:: enterprise_d = Starship(3000) @@ -1365,7 +1365,7 @@ These can be used as types in annotations. They all support subscription using def mutate_movie(m: Movie) -> None: m["year"] = 1999 # allowed - m["title"] = "The Matrix" # typechecker error + m["title"] = "The Matrix" # type checker error There is no runtime checking for this property. @@ -2472,9 +2472,9 @@ types. Fields with a default value must come after any fields without a default. - The resulting class has an extra attribute ``__annotations__`` giving a - dict that maps the field names to the field types. (The field names are in - the ``_fields`` attribute and the default values are in the + The types for each field name can be retrieved by calling + :func:`annotationlib.get_annotations` on the resulting class. (The field + names are in the ``_fields`` attribute and the default values are in the ``_field_defaults`` attribute, both of which are part of the :func:`~collections.namedtuple` API.) @@ -2535,7 +2535,7 @@ types. Helper class to create low-overhead :ref:`distinct types `. - A ``NewType`` is considered a distinct type by a typechecker. At runtime, + A ``NewType`` is considered a distinct type by a type checker. At runtime, however, calling a ``NewType`` returns its argument unchanged. Usage:: @@ -2616,7 +2616,7 @@ types. Mark a protocol class as a runtime protocol. Such a protocol can be used with :func:`isinstance` and :func:`issubclass`. - This allows a simple-minded structural check, very similar to "one trick ponies" + This allows a simple-minded structural check, very similar to "one-trick ponies" in :mod:`collections.abc` such as :class:`~collections.abc.Iterable`. For example:: @runtime_checkable @@ -2855,7 +2855,7 @@ types. key: T group: list[T] - A ``TypedDict`` can be introspected via annotations dicts + A ``TypedDict`` can be introspected via :func:`annotationlib.get_annotations` (see :ref:`annotations-howto` for more information on annotations best practices) and the following attributes: @@ -2898,7 +2898,7 @@ types. For backwards compatibility with Python 3.10 and below, it is also possible to use inheritance to declare both required and - non-required keys in the same ``TypedDict`` . This is done by declaring a + non-required keys in the same ``TypedDict``. This is done by declaring a ``TypedDict`` with one value for the ``total`` argument and then inheriting from it in another ``TypedDict`` with a different value for ``total``: @@ -2982,34 +2982,34 @@ with :deco:`runtime_checkable`. .. class:: SupportsAbs - An ABC with one abstract method ``__abs__`` that is covariant + A protocol with one abstract method ``__abs__`` that is covariant in its return type. .. class:: SupportsBytes - An ABC with one abstract method ``__bytes__``. + A protocol with one abstract method ``__bytes__``. .. class:: SupportsComplex - An ABC with one abstract method ``__complex__``. + A protocol with one abstract method ``__complex__``. .. class:: SupportsFloat - An ABC with one abstract method ``__float__``. + A protocol with one abstract method ``__float__``. .. class:: SupportsIndex - An ABC with one abstract method ``__index__``. + A protocol with one abstract method ``__index__``. .. versionadded:: 3.8 .. class:: SupportsInt - An ABC with one abstract method ``__int__``. + A protocol with one abstract method ``__int__``. .. class:: SupportsRound - An ABC with one abstract method ``__round__`` + A protocol with one abstract method ``__round__`` that is covariant in its return type. .. _typing-io: @@ -3763,7 +3763,7 @@ Constant .. data:: TYPE_CHECKING - A special constant that is assumed to be ``True`` by 3rd party static + A special constant that is assumed to be ``True`` by static type checkers. It's ``False`` at runtime. A module which is expensive to import, and which only contain types diff --git a/Lib/typing.py b/Lib/typing.py index 130e09be4b9127..715d08e0e1603e 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -5,7 +5,7 @@ * Generic, Protocol, and internal machinery to support generic aliases. All subscripted types like X[int], Union[int, str] are generic aliases. * Various "special forms" that have unique meanings in type annotations: - NoReturn, Never, ClassVar, Self, Concatenate, Unpack, and others. + Any, Never, ClassVar, Self, Concatenate, Unpack, and others. * Classes whose instances can be type arguments to generic classes and functions: TypeVar, ParamSpec, TypeVarTuple. * Public helper functions: get_type_hints, overload, cast, final, and others. @@ -591,12 +591,12 @@ def __repr__(self): class Any(metaclass=_AnyMeta): """Special type indicating an unconstrained type. - - Any is compatible with every type. - - Any assumed to have all methods. - - All values assumed to be instances of Any. + - Any is assignable to every type. + - Any assumed to have all methods and attributes. + - All values are assignable to Any. Note that all the above statements are true from the point of view of - static type checkers. At runtime, Any should not be used with instance + static type checkers. At runtime, Any cannot be used with instance checks. """ @@ -715,7 +715,7 @@ class Starship: ClassVar accepts only types and cannot be further subscribed. - Note that ClassVar is not a class itself, and should not + Note that ClassVar is not a class itself, and cannot be used with isinstance() or issubclass(). """ item = _type_check(parameters, f'{self} accepts only single type.', allow_special_forms=True) @@ -745,7 +745,7 @@ class FastConnector(Connection): @_SpecialForm def Optional(self, parameters): - """Optional[X] is equivalent to Union[X, None].""" + """Optional[X] is equivalent to X | None.""" arg = _type_check(parameters, f"{self} requires a single type.") return Union[arg, type(None)] @@ -788,7 +788,7 @@ def open_helper(file: str, mode: MODE) -> str: def TypeAlias(self, parameters): """Special form for marking type aliases. - Use TypeAlias to indicate that an assignment should + TypeAlias can be used to indicate that an assignment should be recognized as a proper type alias definition by type checkers. @@ -1796,7 +1796,7 @@ class Movie(TypedDict): def foo(**kwargs: Unpack[Movie]): ... Note that there is only some runtime checking of this operator. Not - everything the runtime allows may be accepted by static type checkers. + everything the runtime allows is accepted by static type checkers. For more information, see PEPs 646 and 692. """ @@ -2307,7 +2307,7 @@ def runtime_checkable(cls): Such protocol can be used with isinstance() and issubclass(). Raise TypeError if applied to a non-protocol class. This allows a simple-minded structural check very similar to - one trick ponies in collections.abc such as Iterable. + one-trick ponies in collections.abc such as Iterable. For example:: @@ -2377,8 +2377,8 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False, *, format=None): """Return type hints for an object. - This is often the same as obj.__annotations__, but it handles - forward references encoded as string literals and recursively replaces all + This is often the same as annotationlib.get_annotations(obj) or obj.__annotations__, + but it handles forward references encoded as string literals and recursively replaces all 'Annotated[T, ...]' with 'T' (unless 'include_extras=True'). The argument may be a module, class, method, or function. The annotations @@ -2590,7 +2590,7 @@ def get_args(tp): def is_typeddict(tp): - """Check if an annotation is a TypedDict class. + """Check if an object is a TypedDict class. For example:: @@ -2687,10 +2687,10 @@ def _overload_dummy(*args, **kwds): def overload(func): """Decorator for overloaded functions/methods. - In a stub file, place two or more stub definitions for the same - function in a row, each decorated with @overload. - - For example:: + In a non-stub file, place two or more stub definitions for the same + function in a row, each decorated with @overload, followed + by an implementation. The implementation should *not* + be decorated with @overload:: @overload def utf8(value: None) -> None: ... @@ -2698,10 +2698,11 @@ def utf8(value: None) -> None: ... def utf8(value: bytes) -> bytes: ... @overload def utf8(value: str) -> bytes: ... + def utf8(value): + ... # implementation goes here - In a non-stub file (i.e. a regular .py file), do the same but - follow it with an implementation. The implementation should *not* - be decorated with @overload:: + In a stub file or in an abstract method (for example, in a Protocol definition), + the implementation may be omitted:: @overload def utf8(value: None) -> None: ... @@ -2709,8 +2710,6 @@ def utf8(value: None) -> None: ... def utf8(value: bytes) -> bytes: ... @overload def utf8(value: str) -> bytes: ... - def utf8(value): - ... # implementation goes here The overloads for a function can be retrieved at runtime using the get_overloads() function. @@ -2746,7 +2745,7 @@ def final(f): """Decorator to indicate final methods and final classes. Use this decorator to indicate to type checkers that the decorated - method cannot be overridden, and decorated class cannot be subclassed. + method cannot be overridden, and the decorated class cannot be subclassed. For example:: @@ -2811,7 +2810,7 @@ class Disjoint3(Disjoint1, Disjoint2): pass # Type checker error V_co = TypeVar('V_co', covariant=True) # Any type covariant containers. VT_co = TypeVar('VT_co', covariant=True) # Value type covariant containers. T_contra = TypeVar('T_contra', contravariant=True) # Ditto contravariant. -# Internal type variable used for Type[]. +# Internal type bound to class object types. CT_co = TypeVar('CT_co', covariant=True, bound=type) @@ -2899,7 +2898,7 @@ class TeamUser(User): ... And a function that takes a class argument that's a subclass of User and returns an instance of the corresponding class:: - def new_user[U](user_class: Type[U]) -> U: + def new_user[U](user_class: type[U]) -> U: user = user_class() # (Here we could write the user object to a database) return user @@ -2912,7 +2911,7 @@ def new_user[U](user_class: Type[U]) -> U: @runtime_checkable class SupportsInt(Protocol): - """An ABC with one abstract method __int__.""" + """A protocol with one abstract method __int__.""" __slots__ = () @@ -2923,7 +2922,7 @@ def __int__(self) -> int: @runtime_checkable class SupportsFloat(Protocol): - """An ABC with one abstract method __float__.""" + """A protocol with one abstract method __float__.""" __slots__ = () @@ -2934,7 +2933,7 @@ def __float__(self) -> float: @runtime_checkable class SupportsComplex(Protocol): - """An ABC with one abstract method __complex__.""" + """A protocol with one abstract method __complex__.""" __slots__ = () @@ -2945,7 +2944,7 @@ def __complex__(self) -> complex: @runtime_checkable class SupportsBytes(Protocol): - """An ABC with one abstract method __bytes__.""" + """A protocol with one abstract method __bytes__.""" __slots__ = () @@ -2956,7 +2955,7 @@ def __bytes__(self) -> bytes: @runtime_checkable class SupportsIndex(Protocol): - """An ABC with one abstract method __index__.""" + """A protocol with one abstract method __index__.""" __slots__ = () @@ -2967,7 +2966,7 @@ def __index__(self) -> int: @runtime_checkable class SupportsAbs[T](Protocol): - """An ABC with one abstract method __abs__ that is covariant in its return type.""" + """A protocol with one abstract method __abs__ that is covariant in its return type.""" __slots__ = () @@ -2978,7 +2977,7 @@ def __abs__(self) -> T: @runtime_checkable class SupportsRound[T](Protocol): - """An ABC with one abstract method __round__ that is covariant in its return type.""" + """A protocol with one abstract method __round__ that is covariant in its return type.""" __slots__ = () @@ -3095,7 +3094,7 @@ def annotate(format): def NamedTuple(typename, fields, /): - """Typed version of namedtuple. + """Typed version of collections.namedtuple. Usage:: @@ -3107,8 +3106,8 @@ class Employee(NamedTuple): Employee = collections.namedtuple('Employee', ['name', 'id']) - The resulting class has an extra __annotations__ attribute, giving a - dict that maps field names to types. (The field names are also in + The types for each field name can be retrieved by calling + annotationlib.get_annotations(Employee). (The field names are also in the _fields attribute, which is part of the namedtuple API.) An alternative equivalent functional syntax is also accepted:: @@ -3161,7 +3160,7 @@ def __new__(cls, name, bases, ns, total=True, closed=None, This method is called when TypedDict is subclassed, or when TypedDict is instantiated. This way - TypedDict supports all three syntax forms described in its docstring. + TypedDict classes can be created through both class-based and functional syntax. Subclasses and instances of TypedDict return actual dictionaries. """ for base in bases: @@ -3315,14 +3314,22 @@ def TypedDict(typename, fields, /, *, total=True, closed=None, >>> Point2D(x=1, y=2, label='first') == dict(x=1, y=2, label='first') True - The type info can be accessed via the Point2D.__annotations__ dict, and - the Point2D.__required_keys__ and Point2D.__optional_keys__ frozensets. + The type info can be accessed by calling annotationlib.get_annotations(Point2D), and + via the Point2D.__required_keys__ and Point2D.__optional_keys__ frozensets. TypedDict supports an additional equivalent form:: Point2D = TypedDict('Point2D', {'x': int, 'y': int, 'label': str}) By default, all keys must be present in a TypedDict. It is possible - to override this by specifying totality:: + to override this by using the NotRequired and Required special forms:: + + class Point2D(TypedDict): + x: int # the "x" key must always be present (Required is the default) + y: NotRequired[int] # the "y" key can be omitted + + This means that a Point2D TypedDict can have the "y" key omitted, but the "x" key must be present. + Items are required by default, so the Required special form is not necessary in this example. + In addition, the total argument to the TypedDict function can be used to make all items not required:: class Point2D(TypedDict, total=False): x: int @@ -3331,16 +3338,8 @@ class Point2D(TypedDict, total=False): This means that a Point2D TypedDict can have any of the keys omitted. A type checker is only expected to support a literal False or True as the value of the total argument. True is the default, and makes all items defined in the - class body be required. - - The Required and NotRequired special forms can also be used to mark - individual keys as being required or not required:: - - class Point2D(TypedDict): - x: int # the "x" key must always be present (Required is the default) - y: NotRequired[int] # the "y" key can be omitted - - See PEP 655 for more details on Required and NotRequired. + class body be required. The Required special form can be used to mark individual + keys as required in a total=False TypedDict. The ReadOnly special form can be used to mark individual keys as immutable for type checkers:: @@ -3374,7 +3373,7 @@ class Point3D(Point2D): by default, and it may not be used with the closed argument at the same time. - See PEP 728 for more information about closed and extra_items. + See PEPs 589, 655, 705, and 728 for more information. """ ns = {'__annotations__': dict(fields)} module = _caller() @@ -3404,7 +3403,7 @@ class Movie(TypedDict, total=False): year: int m = Movie( - title='The Matrix', # typechecker error if key is omitted + title='The Matrix', # type checker error if key is omitted year=1999, ) @@ -3426,7 +3425,7 @@ class Movie(TypedDict): year: NotRequired[int] m = Movie( - title='The Matrix', # typechecker error if key is omitted + title='The Matrix', # type checker error if key is omitted year=1999, ) """ @@ -3446,7 +3445,7 @@ class Movie(TypedDict): def mutate_movie(m: Movie) -> None: m["year"] = 1992 # allowed - m["title"] = "The Matrix" # typechecker error + m["title"] = "The Matrix" # type checker error There is no runtime checking for this property. """ @@ -3533,8 +3532,8 @@ class IO(Generic[AnyStr]): classes (text vs. binary, read vs. write vs. read/write, append-only, unbuffered). The TextIO and BinaryIO subclasses below capture the distinctions between text vs. binary, which is - pervasive in the interface; however we currently do not offer a - way to track the other distinctions in the type system. + pervasive in the interface. For more precise types, define a custom + Protocol. """ __slots__ = () @@ -3624,7 +3623,7 @@ def __exit__(self, type, value, traceback, /) -> None: class BinaryIO(IO[bytes]): - """Typed version of the return of open() in binary mode.""" + """Typed approximation of the return of open() in binary mode.""" __slots__ = () @@ -3638,7 +3637,7 @@ def __enter__(self) -> BinaryIO: class TextIO(IO[str]): - """Typed version of the return of open() in text mode.""" + """Typed approximation of the return of open() in text mode.""" __slots__ = () @@ -3705,7 +3704,7 @@ def dataclass_transform( field_specifiers: tuple[type[Any] | Callable[..., Any], ...] = (), **kwargs: Any, ) -> _IdentityCallable: - """Decorator to mark an object as providing dataclass-like behaviour. + """Decorator to mark an object as providing dataclass-like behavior. The decorator can be applied to a function, class, or metaclass. diff --git a/Misc/NEWS.d/next/Library/2026-05-18-07-44-46.gh-issue-149995.vvtFHn.rst b/Misc/NEWS.d/next/Library/2026-05-18-07-44-46.gh-issue-149995.vvtFHn.rst new file mode 100644 index 00000000000000..a8e412b578da37 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-18-07-44-46.gh-issue-149995.vvtFHn.rst @@ -0,0 +1 @@ +Update various docstrings in :mod:`typing`. From 6b3c418b4c504729cc7cef9188a18fdad7727c68 Mon Sep 17 00:00:00 2001 From: adang1345 Date: Fri, 22 May 2026 00:20:08 -0700 Subject: [PATCH 10/15] gh-133998: Fix gzip file creation when time is out of range (GH-134278) Co-authored-by: Serhiy Storchaka --- Doc/library/gzip.rst | 10 ++++++--- Lib/gzip.py | 10 +++++++-- Lib/test/test_gzip.py | 21 +++++++++++++++++++ ...-05-19-20-29-35.gh-issue-133998.KmElUw.rst | 5 +++++ 4 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-05-19-20-29-35.gh-issue-133998.KmElUw.rst diff --git a/Doc/library/gzip.rst b/Doc/library/gzip.rst index ed9fdaf1d727b0..2c667ddc522399 100644 --- a/Doc/library/gzip.rst +++ b/Doc/library/gzip.rst @@ -108,9 +108,13 @@ The module defines the following items: is no compression. The default is ``9``. The optional *mtime* argument is the timestamp requested by gzip. The time - is in Unix format, i.e., seconds since 00:00:00 UTC, January 1, 1970. - If *mtime* is omitted or ``None``, the current time is used. Use *mtime* = 0 - to generate a compressed stream that does not depend on creation time. + is in Unix format, i.e., seconds since 00:00:00 UTC, January 1, 1970. Set + *mtime* to ``0`` to generate a compressed stream that does not depend on + creation time. If *mtime* is omitted or ``None``, the current time is used; + however, if the current time is outside the range 00:00:00 UTC, January 1, + 1970 through 06:28:15 UTC, February 7, 2106, or explicitly passed *mtime* + argument is outside the range ``0`` to ``2**32-1``, then the value ``0`` + is used instead. See below for the :attr:`mtime` attribute that is set when decompressing. diff --git a/Lib/gzip.py b/Lib/gzip.py index 1e05f43c0c9e24..8720acc4db9976 100644 --- a/Lib/gzip.py +++ b/Lib/gzip.py @@ -188,8 +188,10 @@ def __init__(self, filename=None, mode=None, The optional mtime argument is the timestamp requested by gzip. The time is in Unix format, i.e., seconds since 00:00:00 UTC, January 1, 1970. - If mtime is omitted or None, the current time is used. Use mtime = 0 - to generate a compressed stream that does not depend on creation time. + Set mtime to 0 to generate a compressed stream that does not depend on + creation time. If mtime is omitted or None, the current time is used. + If the resulting mtime is outside the range 0 to 2**32-1, then the + value 0 is used instead. """ @@ -295,6 +297,8 @@ def _write_gzip_header(self, compresslevel): mtime = self._write_mtime if mtime is None: mtime = time.time() + if not 0 <= mtime < 2**32: + mtime = 0 write32u(self.fileobj, int(mtime)) if compresslevel == _COMPRESS_LEVEL_BEST: xfl = b'\002' @@ -663,6 +667,8 @@ def compress(data, compresslevel=_COMPRESS_LEVEL_TRADEOFF, *, mtime=0): gzip_data = zlib.compress(data, level=compresslevel, wbits=31) if mtime is None: mtime = time.time() + if not 0 <= mtime < 2**32: + mtime = 0 # Reuse gzip header created by zlib, replace mtime and OS byte for # consistency. header = struct.pack("<4sLBB", gzip_data, int(mtime), gzip_data[8], 255) diff --git a/Lib/test/test_gzip.py b/Lib/test/test_gzip.py index b3b7c8f87e4f9f..cafac9d3c8be6e 100644 --- a/Lib/test/test_gzip.py +++ b/Lib/test/test_gzip.py @@ -10,6 +10,7 @@ import sys import unittest from subprocess import PIPE, Popen +from unittest import mock from test.support import catch_unraisable_exception from test.support import force_not_colorized_test_class, import_helper from test.support import os_helper @@ -350,6 +351,26 @@ def test_mtime(self): self.assertEqual(dataRead, data1) self.assertEqual(fRead.mtime, mtime) + def test_mtime_out_of_range(self): + for mtime in (-1, 2**32): + with gzip.GzipFile(self.filename, 'w', mtime=mtime) as fWrite: + fWrite.write(data1) + with gzip.GzipFile(self.filename) as fRead: + fRead.read(1) + self.assertEqual(fRead.mtime, 0) + datac = gzip.compress(data1, mtime=mtime) + with gzip.GzipFile(fileobj=io.BytesIO(datac)) as fRead: + fRead.read(1) + self.assertEqual(fRead.mtime, 0) + + for mtime in (-1, 2**32): + with mock.patch('time.time', return_value=float(mtime)): + with gzip.GzipFile(self.filename, 'w') as fWrite: + fWrite.write(data1) + with gzip.GzipFile(self.filename) as fRead: + fRead.read(1) + self.assertEqual(fRead.mtime, 0) + def test_metadata(self): mtime = 123456789 diff --git a/Misc/NEWS.d/next/Library/2025-05-19-20-29-35.gh-issue-133998.KmElUw.rst b/Misc/NEWS.d/next/Library/2025-05-19-20-29-35.gh-issue-133998.KmElUw.rst new file mode 100644 index 00000000000000..77d92628beefac --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-05-19-20-29-35.gh-issue-133998.KmElUw.rst @@ -0,0 +1,5 @@ +Fix :exc:`struct.error` exception when creating a file with +:class:`gzip.GzipFile` or compressing data with :func:`gzip.compress` +if the system time is outside the range 00:00:00 UTC, January 1, 1970 +through 06:28:15 UTC, February 7, 2106, or explicitly passed *mtime* +argument is outside the range ``0`` to ``2**32-1``. From 0f8420523e913f0af1a959e9ae6b4170a6947bf9 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 22 May 2026 12:17:34 +0300 Subject: [PATCH 11/15] gh-137571: Protect against possible UnboundLocalError in gzip._GzipReader.read() (GH-150222) This has not been observed in practice, but we cannot be 100% sure that it will not happen with some weird gzip data. --- Lib/gzip.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/gzip.py b/Lib/gzip.py index 8720acc4db9976..0713b922522ee1 100644 --- a/Lib/gzip.py +++ b/Lib/gzip.py @@ -610,10 +610,10 @@ def read(self, size=-1): # Read a chunk of data from the file if self._decompressor.needs_input: buf = self._fp.read(READ_BUFFER_SIZE) - uncompress = self._decompressor.decompress(buf, size) else: - uncompress = self._decompressor.decompress(b"", size) + buf = b"" + uncompress = self._decompressor.decompress(buf, size) if self._decompressor.unused_data != b"": # Prepend the already read bytes to the fileobj so they can # be seen by _read_eof() and _read_gzip_header() From 68a27652debc7fb2a3bcf60741ca92142cfc071e Mon Sep 17 00:00:00 2001 From: Mia Albert Date: Fri, 22 May 2026 07:32:14 -0400 Subject: [PATCH 12/15] gh-149902: Remove dead packaging docs link and add a new section for external resources (#150030) Co-authored-by: Stan Ulbrych Co-authored-by: Ned Batchelder Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Doc/tools/templates/indexcontent.html | 152 ++++++++++++++++---------- 1 file changed, 97 insertions(+), 55 deletions(-) diff --git a/Doc/tools/templates/indexcontent.html b/Doc/tools/templates/indexcontent.html index 544cc4234f441e..4982bcbfe3673c 100644 --- a/Doc/tools/templates/indexcontent.html +++ b/Doc/tools/templates/indexcontent.html @@ -14,6 +14,35 @@ + {%- endblock -%} {% block body %}

{{ docstitle|e }}

@@ -21,63 +50,76 @@

{{ docstitle|e }}

{% trans %}Welcome! This is the official documentation for Python {{ release }}.{% endtrans %}

{% trans %}Documentation sections:{% endtrans %}

- - -
- - - - - - - - - - - - - -
+
+ + +
+ +

{% trans %}Other resources:{% endtrans %}

+
+ + +

{% trans %}Indices, glossary, and search:{% endtrans %}

- - -
- - - - - - -
+
+ + +

{% trans %}Project information:{% endtrans %}

- - -
- - - - - - - -
+ {% endblock %} From 344f35d404408e115212bf834aa15443fe7ae693 Mon Sep 17 00:00:00 2001 From: Marin Misur <50244077+ellaellela@users.noreply.github.com> Date: Fri, 22 May 2026 14:14:25 +0200 Subject: [PATCH 13/15] gh-91372: Add mtime to gzip.open() (GH-32310) --- Doc/library/gzip.rst | 13 ++++++++++--- Doc/whatsnew/3.16.rst | 8 ++++++++ Lib/gzip.py | 7 ++++--- Lib/test/test_gzip.py | 11 +++++++++++ .../2022-04-04-17-58-05.bpo-47216.gPyPte.rst | 2 ++ 5 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2022-04-04-17-58-05.bpo-47216.gPyPte.rst diff --git a/Doc/library/gzip.rst b/Doc/library/gzip.rst index 2c667ddc522399..9211e5f18c6b6e 100644 --- a/Doc/library/gzip.rst +++ b/Doc/library/gzip.rst @@ -28,7 +28,7 @@ Note that additional file formats which can be decompressed by the The module defines the following items: -.. function:: open(filename, mode='rb', compresslevel=6, encoding=None, errors=None, newline=None) +.. function:: open(filename, mode='rb', compresslevel=6, encoding=None, errors=None, newline=None, *, mtime=None) Open a gzip-compressed file in binary or text mode, returning a :term:`file object`. @@ -43,9 +43,12 @@ The module defines the following items: The *compresslevel* argument is an integer from 0 to 9, as for the :class:`GzipFile` constructor. + The keyword-only argument *mtime* represents a Unix timestamp. + For binary mode, this function is equivalent to the :class:`GzipFile` - constructor: ``GzipFile(filename, mode, compresslevel)``. In this case, the - *encoding*, *errors* and *newline* arguments must not be provided. + constructor: ``GzipFile(filename, mode, compresslevel, mtime=mtime)``. + In this case, the *encoding*, *errors* and *newline* arguments must not + be provided. For text mode, a :class:`GzipFile` object is created, and wrapped in an :class:`io.TextIOWrapper` instance with the specified encoding, error @@ -66,6 +69,10 @@ The module defines the following items: It is the default level used by most compression tools and a better tradeoff between speed and performance. + .. versionchanged:: next + Added keyword-only argument *mtime* which is passed to the class + constructor of :class:`~gzip.GzipFile`. + .. exception:: BadGzipFile An exception raised for invalid gzip files. It inherits from :exc:`OSError`. diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 2e5342e4f02053..f1ff4fcf9bafde 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -86,6 +86,14 @@ New modules Improved modules ================ + +gzip +---- + +* :func:`gzip.open` now accepts an optional argument ``mtime`` + which is passed on to the constructor of the :class:`~gzip.GzipFile` class. + (Contributed by Marin Misur in :gh:`91372`.) + os -- diff --git a/Lib/gzip.py b/Lib/gzip.py index 0713b922522ee1..14c47fc86f217a 100644 --- a/Lib/gzip.py +++ b/Lib/gzip.py @@ -31,7 +31,7 @@ def open(filename, mode="rb", compresslevel=_COMPRESS_LEVEL_TRADEOFF, - encoding=None, errors=None, newline=None): + encoding=None, errors=None, newline=None, *, mtime=None): """Open a gzip-compressed file in binary or text mode. The filename argument can be an actual filename (a str or bytes object), or @@ -63,9 +63,10 @@ def open(filename, mode="rb", compresslevel=_COMPRESS_LEVEL_TRADEOFF, gz_mode = mode.replace("t", "") if isinstance(filename, (str, bytes, os.PathLike)): - binary_file = GzipFile(filename, gz_mode, compresslevel) + binary_file = GzipFile(filename, gz_mode, compresslevel, mtime=mtime) elif hasattr(filename, "read") or hasattr(filename, "write"): - binary_file = GzipFile(None, gz_mode, compresslevel, filename) + binary_file = GzipFile(None, gz_mode, compresslevel, filename, + mtime=mtime) else: raise TypeError("filename must be a str or bytes object, or a file") diff --git a/Lib/test/test_gzip.py b/Lib/test/test_gzip.py index cafac9d3c8be6e..8bc8e507683cb5 100644 --- a/Lib/test/test_gzip.py +++ b/Lib/test/test_gzip.py @@ -351,6 +351,17 @@ def test_mtime(self): self.assertEqual(dataRead, data1) self.assertEqual(fRead.mtime, mtime) + def test_mtime_with_open(self): + mtime = 123456789 + with gzip.open(self.filename, "wb", mtime=mtime) as fWrite: + fWrite.write(data1) + with gzip.open(self.filename, "rb") as fRead: + self.assertTrue(hasattr(fRead, 'mtime')) + self.assertIsNone(fRead.mtime) + dataRead = fRead.read() + self.assertEqual(dataRead, data1) + self.assertEqual(fRead.mtime, mtime) + def test_mtime_out_of_range(self): for mtime in (-1, 2**32): with gzip.GzipFile(self.filename, 'w', mtime=mtime) as fWrite: diff --git a/Misc/NEWS.d/next/Library/2022-04-04-17-58-05.bpo-47216.gPyPte.rst b/Misc/NEWS.d/next/Library/2022-04-04-17-58-05.bpo-47216.gPyPte.rst new file mode 100644 index 00000000000000..066e17c1c6e06a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-04-04-17-58-05.bpo-47216.gPyPte.rst @@ -0,0 +1,2 @@ +Added *mtime* option to :func:`gzip.open`, which will be passed +to the constructor of :class:`~gzip.GzipFile`. From 403322e15165d9ae294ec035aa7813a332d99abc Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 22 May 2026 15:47:38 +0200 Subject: [PATCH 14/15] gh-149879: Fix test_c_locale_coercion on Cygwin (#150250) --- Lib/test/test_c_locale_coercion.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_c_locale_coercion.py b/Lib/test/test_c_locale_coercion.py index 340bec3c71b68f..52323a0ec0ac2b 100644 --- a/Lib/test/test_c_locale_coercion.py +++ b/Lib/test/test_c_locale_coercion.py @@ -47,9 +47,8 @@ # FS encoding is UTF-8 on macOS EXPECTED_C_LOCALE_FS_ENCODING = "utf-8" elif sys.platform == "cygwin": - # Cygwin defaults to using C.UTF-8 - # TODO: Work out a robust dynamic test for this that doesn't rely on - # CPython's own locale handling machinery + DEFAULT_LOCALE_IS_C = False + DEFAULT_ENCODING = "utf-8" EXPECT_COERCION_IN_DEFAULT_LOCALE = False elif sys.platform == "vxworks": # VxWorks defaults to using UTF-8 for all system interfaces From 7b50ffcaabe83f0936f96012c390fb37cf8b71f2 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 22 May 2026 07:21:16 -0700 Subject: [PATCH 15/15] Remove 'expat' dependency for Linux in `Misc/Brewfile` (#150118) --- Misc/Brewfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Misc/Brewfile b/Misc/Brewfile index b62c6e943989a0..c799f099957f75 100644 --- a/Misc/Brewfile +++ b/Misc/Brewfile @@ -7,7 +7,6 @@ brew "xz" brew "zstd" brew "bzip2" if OS.linux? -brew "expat" if OS.linux? brew "libedit" if OS.linux? brew "libffi" if OS.linux? brew "ncurses" if OS.linux?