From 4ec7ef266ddd016631b48f944d62a3619463e898 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 21 Oct 2026 14:22:58 -0700 Subject: [PATCH] gh-100609: Render an unnamed zipfile as ":zipfile:" in `zipfile.Path` (GH-160103) Synchronizes the fix from zipp 3.23.1. `str()` of a `zipfile.Path` whose zipfile has no filename previously raised `TypeError`. (cherry picked from commit ba96fab7348310651dcabadc1f4e954949e0e05a) Co-authored-by: Jason R. Coombs Co-authored-by: Claude Opus 5 --- Lib/test/test_zipfile/_path/test_path.py | 15 +++++++++++++++ Lib/zipfile/_path/__init__.py | 7 ++++--- Lib/zipfile/_path/_functools.py | 11 +++++++++++ ...2026-10-21-19-40-00.gh-issue-100609.THXgRC.rst | 5 +++++ 4 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-21-19-40-00.gh-issue-100609.THXgRC.rst diff --git a/Lib/test/test_zipfile/_path/test_path.py b/Lib/test/test_zipfile/_path/test_path.py index e7931b6f394075..a6755ffa337c10 100644 --- a/Lib/test/test_zipfile/_path/test_path.py +++ b/Lib/test/test_zipfile/_path/test_path.py @@ -351,6 +351,21 @@ class Subclass(zipfile.Path): root = Subclass(alpharep) assert isinstance(root / 'b', Subclass) + def test_at(self): + zip_file = self.enterContext(zipfile.ZipFile(io.BytesIO(), 'w')) + root = zipfile.Path(zip_file) + assert root.at == '' + assert (root / 'foo' / 'bar').at == 'foo/bar' + + def test_str(self): + """ + An unnamed zip file is indicated by ":zipfile:" + """ + zip_file = self.enterContext(zipfile.ZipFile(io.BytesIO(), 'w')) + root = zipfile.Path(zip_file) + assert str(root) == ':zipfile:' + assert str(root / 'foo' / 'bar') == ':zipfile:/foo/bar' + @pass_alpharep def test_filename(self, alpharep): root = zipfile.Path(alpharep) diff --git a/Lib/zipfile/_path/__init__.py b/Lib/zipfile/_path/__init__.py index faae4c84cae5ed..010eeed95d2707 100644 --- a/Lib/zipfile/_path/__init__.py +++ b/Lib/zipfile/_path/__init__.py @@ -17,7 +17,7 @@ import sys import zipfile -from ._functools import save_method_args +from ._functools import none_as, save_method_args from .glob import Translator __all__ = ['Path'] @@ -276,7 +276,7 @@ class Path: resolve to the zipfile. >>> str(path) - 'mem/abcde.zip/' + 'mem/abcde.zip' >>> path.name 'abcde.zip' >>> path.filename == pathlib.Path('mem/abcde.zip') @@ -434,7 +434,8 @@ def relative_to(self, other, *extra): return posixpath.relpath(str(self), str(other.joinpath(*extra))) def __str__(self): - return posixpath.join(self.root.filename, self.at) + root = none_as(self.root.filename, ':zipfile:') + return posixpath.join(root, self.at) if self.at else root def __repr__(self): return self.__repr.format(self=self) diff --git a/Lib/zipfile/_path/_functools.py b/Lib/zipfile/_path/_functools.py index 7390be21873e4b..7d82636ac8d627 100644 --- a/Lib/zipfile/_path/_functools.py +++ b/Lib/zipfile/_path/_functools.py @@ -18,3 +18,14 @@ def wrapper(self, /, *args, **kwargs): return method(self, *args, **kwargs) return wrapper + + +# from jaraco.functools 4.3 +def none_as(value, replacement=None): + """ + >>> none_as(None, 'foo') + 'foo' + >>> none_as('bar', 'foo') + 'bar' + """ + return replacement if value is None else value diff --git a/Misc/NEWS.d/next/Library/2026-10-21-19-40-00.gh-issue-100609.THXgRC.rst b/Misc/NEWS.d/next/Library/2026-10-21-19-40-00.gh-issue-100609.THXgRC.rst new file mode 100644 index 00000000000000..4a793a0fdcd91e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-21-19-40-00.gh-issue-100609.THXgRC.rst @@ -0,0 +1,5 @@ +``str()`` of a :class:`zipfile.Path` whose underlying zipfile has no +filename, such as one constructed from an :class:`io.BytesIO`, now renders +that filename as ``:zipfile:`` instead of raising :exc:`TypeError`. The root +of a zipfile is also no longer rendered with a trailing slash. Synchronized +with :pypi:`zipp` 3.23.1.