https://github.com/python/cpython/commit/ba96fab7348310651dcabadc1f4e954949e0e05a
commit: ba96fab7348310651dcabadc1f4e954949e0e05a
branch: main
author: Jason R. Coombs <[email protected]>
committer: jaraco <[email protected]>
date: 2026-10-21T21:22:58Z
summary:

gh-100609: Render an unnamed zipfile as ":zipfile:" in `zipfile.Path` (#160103)

Synchronizes the fix from zipp 3.23.1. `str()` of a `zipfile.Path` whose
zipfile has no filename previously raised `TypeError`.

Co-authored-by: Claude Opus 5 <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-10-21-19-40-00.gh-issue-100609.THXgRC.rst
M Lib/test/test_zipfile/_path/test_path.py
M Lib/zipfile/_path/__init__.py
M Lib/zipfile/_path/_functools.py

diff --git a/Lib/test/test_zipfile/_path/test_path.py 
b/Lib/test/test_zipfile/_path/test_path.py
index b00491ab4cbdb77..663f2862bca9028 100644
--- a/Lib/test/test_zipfile/_path/test_path.py
+++ b/Lib/test/test_zipfile/_path/test_path.py
@@ -347,6 +347,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 faae4c84cae5ed2..010eeed95d2707e 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 7390be21873e4ba..7d82636ac8d627a 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 000000000000000..4a793a0fdcd91e8
--- /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.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to