From 190b3ef2cbf7ccae03c2a47ffe36ea8be2966a97 Mon Sep 17 00:00:00 2001 From: Grant Herman Date: Tue, 19 May 2026 19:22:28 -0400 Subject: [PATCH 1/9] fix(150075): adding tarinfo offset info before and after reading the file Signed-off-by: Grant Herman --- Lib/tarfile.py | 5 ++++- Lib/test/test_tarfile.py | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Lib/tarfile.py b/Lib/tarfile.py index 87500c726ce9a8..5c39a84b5ce855 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -2360,11 +2360,14 @@ def addfile(self, tarinfo, fileobj=None): raise ValueError("fileobj not provided for non zero-size regular file") tarinfo = copy.copy(tarinfo) - + # get current offset + tarinfo.offset = self.offset buf = tarinfo.tobuf(self.format, self.encoding, self.errors) self.fileobj.write(buf) self.offset += len(buf) + # add original offset to block size bufsize=self.copybufsize + tarinfo.offset_data = self.offset # If there's data to follow, append it. if fileobj is not None: copyfileobj(fileobj, self.fileobj, tarinfo.size, bufsize=bufsize) diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index 02fd9620bcf33d..ca778bd12ab801 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -1456,6 +1456,29 @@ class WriteTest(WriteTestBase, unittest.TestCase): prefix = "w:" + def test_addfile_sets_offsets(self): + # gh-150075: addfile() must set offset and offset_data on the + # TarInfo stored in the archive so they match a subsequent read. + data = b"data" + + with tarfile.open(tmpname, self.mode) as tar: + t1 = tarfile.TarInfo("test1.txt") + t1.size = len(data) + tar.addfile(t1, io.BytesIO(data)) + + t2 = tarfile.TarInfo("test2.txt") + t2.size = len(data) + tar.addfile(t2, io.BytesIO(data)) + + write_members = tar.getmembers() + + with tarfile.open(tmpname) as tar: + read_members = tar.getmembers() + + for w, r in zip(write_members, read_members): + self.assertEqual(w.offset, r.offset) + self.assertEqual(w.offset_data, r.offset_data) + def test_100_char_name(self): # The name field in a tar header stores strings of at most 100 chars. # If a string is shorter than 100 chars it has to be padded with '\0', From 2705a52c17c0ffb52ed3611bf1704f9ceee8e781 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:21:08 +0000 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-05-21-14-21-06.gh-issue-150075.dLI21Z.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-05-21-14-21-06.gh-issue-150075.dLI21Z.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-21-14-21-06.gh-issue-150075.dLI21Z.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-21-14-21-06.gh-issue-150075.dLI21Z.rst new file mode 100644 index 00000000000000..bf04e688025c71 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-21-14-21-06.gh-issue-150075.dLI21Z.rst @@ -0,0 +1 @@ +tar.addfile() doesn't set member offsets in 3.15. From reviewing the file it seemed like the copy was not adding the proper offsets to the tarinfo object. The way I fixed this is that I added the classes current offset of when.addFile is called and then added the block size after .tobuf function was called. From 982749a62f7766bee945915bf395ea8c1b61daca Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 21 May 2026 21:06:42 -0700 Subject: [PATCH 3/9] 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 6807e9d03fc68d0af2fc5a15cef667ef8756a5ca Mon Sep 17 00:00:00 2001 From: adang1345 Date: Fri, 22 May 2026 00:20:08 -0700 Subject: [PATCH 4/9] 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 41f1cdca409eb98984f1a45b94b8ee6d08e2cdc7 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 22 May 2026 12:17:34 +0300 Subject: [PATCH 5/9] 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 d69206097c654e6f082375edfe47364cf9b05629 Mon Sep 17 00:00:00 2001 From: Mia Albert Date: Fri, 22 May 2026 07:32:14 -0400 Subject: [PATCH 6/9] 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 808ea130b9ac387724c7c153b2f5026a59f919b5 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 7/9] 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 16d1813873ec2261a42ef04d8d97b92b8915fefc Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 22 May 2026 15:47:38 +0200 Subject: [PATCH 8/9] 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 f26b7bb33d53c240e99548caee3422276a5ba53c Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 22 May 2026 07:21:16 -0700 Subject: [PATCH 9/9] 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?