]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-119588: Implement zipfile.Path.is_symlink (zipp 3.19.0). (#119591)
authorJason R. Coombs <jaraco@jaraco.com>
Mon, 3 Jun 2024 15:13:07 +0000 (11:13 -0400)
committerGitHub <noreply@github.com>
Mon, 3 Jun 2024 15:13:07 +0000 (11:13 -0400)
Doc/library/zipfile.rst
Lib/test/test_zipfile/_path/test_path.py
Lib/zipfile/_path/__init__.py
Misc/NEWS.d/next/Library/2024-05-26-21-28-11.gh-issue-119588.wlLBK5.rst [new file with mode: 0644]

index aad2028523dc341767828d56fd424fb6c0d76d03..a4d9a1852f8f0dbcc193fa0a2c8c7a3398816d52 100644 (file)
@@ -585,6 +585,15 @@ Path objects are traversable using the ``/`` operator or ``joinpath``.
 
    Return ``True`` if the current context references a file.
 
+.. method:: Path.is_symlink()
+
+   Return ``True`` if the current context references a symbolic link.
+
+   .. versionadded:: 3.12
+
+   .. versionchanged:: 3.12.4
+      Prior to 3.12.4, ``is_symlink`` would unconditionally return ``False``.
+
 .. method:: Path.exists()
 
    Return ``True`` if the current context references a file or
index e5d2acf39a10f8d051de9b3ee230320a2aa501d0..99842ffd63a64efade5953936beabb74a9bab599 100644 (file)
@@ -3,6 +3,7 @@ import itertools
 import contextlib
 import pathlib
 import pickle
+import stat
 import sys
 import unittest
 import zipfile
@@ -21,12 +22,17 @@ class jaraco:
         Counter = Counter
 
 
+def _make_link(info: zipfile.ZipInfo):  # type: ignore[name-defined]
+    info.external_attr |= stat.S_IFLNK << 16
+
+
 def build_alpharep_fixture():
     """
     Create a zip file with this structure:
 
     .
     ├── a.txt
+    ├── n.txt (-> a.txt)
     ├── b
     │   ├── c.txt
     │   ├── d
@@ -47,6 +53,7 @@ def build_alpharep_fixture():
     - multiple files in a directory (b/c, b/f)
     - a directory containing only a directory (g/h)
     - a directory with files of different extensions (j/klm)
+    - a symlink (n) pointing to (a)
 
     "alpha" because it uses alphabet
     "rep" because it's a representative example
@@ -61,6 +68,9 @@ def build_alpharep_fixture():
     zf.writestr("j/k.bin", b"content of k")
     zf.writestr("j/l.baz", b"content of l")
     zf.writestr("j/m.bar", b"content of m")
+    zf.writestr("n.txt", b"a.txt")
+    _make_link(zf.infolist()[-1])
+
     zf.filename = "alpharep.zip"
     return zf
 
@@ -91,7 +101,7 @@ class TestPath(unittest.TestCase):
     def test_iterdir_and_types(self, alpharep):
         root = zipfile.Path(alpharep)
         assert root.is_dir()
-        a, b, g, j = root.iterdir()
+        a, k, b, g, j = root.iterdir()
         assert a.is_file()
         assert b.is_dir()
         assert g.is_dir()
@@ -111,7 +121,7 @@ class TestPath(unittest.TestCase):
     @pass_alpharep
     def test_iterdir_on_file(self, alpharep):
         root = zipfile.Path(alpharep)
-        a, b, g, j = root.iterdir()
+        a, k, b, g, j = root.iterdir()
         with self.assertRaises(ValueError):
             a.iterdir()
 
@@ -126,7 +136,7 @@ class TestPath(unittest.TestCase):
     @pass_alpharep
     def test_open(self, alpharep):
         root = zipfile.Path(alpharep)
-        a, b, g, j = root.iterdir()
+        a, k, b, g, j = root.iterdir()
         with a.open(encoding="utf-8") as strm:
             data = strm.read()
         self.assertEqual(data, "content of a")
@@ -230,7 +240,7 @@ class TestPath(unittest.TestCase):
     @pass_alpharep
     def test_read(self, alpharep):
         root = zipfile.Path(alpharep)
-        a, b, g, j = root.iterdir()
+        a, k, b, g, j = root.iterdir()
         assert a.read_text(encoding="utf-8") == "content of a"
         # Also check positional encoding arg (gh-101144).
         assert a.read_text("utf-8") == "content of a"
@@ -296,7 +306,7 @@ class TestPath(unittest.TestCase):
         reflect that change.
         """
         root = zipfile.Path(alpharep)
-        a, b, g, j = root.iterdir()
+        a, k, b, g, j = root.iterdir()
         alpharep.writestr('foo.txt', 'foo')
         alpharep.writestr('bar/baz.txt', 'baz')
         assert any(child.name == 'foo.txt' for child in root.iterdir())
@@ -513,12 +523,9 @@ class TestPath(unittest.TestCase):
 
     @pass_alpharep
     def test_is_symlink(self, alpharep):
-        """
-        See python/cpython#82102 for symlink support beyond this object.
-        """
-
         root = zipfile.Path(alpharep)
-        assert not root.is_symlink()
+        assert not root.joinpath('a.txt').is_symlink()
+        assert root.joinpath('n.txt').is_symlink()
 
     @pass_alpharep
     def test_relative_to(self, alpharep):
index 79ebb777354e03285b38ba969876dc0261d59f53..f5ea18cee61930dea8cfcef5ddb4644c54b0d8cb 100644 (file)
@@ -5,6 +5,7 @@ import itertools
 import contextlib
 import pathlib
 import re
+import stat
 import sys
 
 from .glob import Translator
@@ -390,9 +391,11 @@ class Path:
 
     def is_symlink(self):
         """
-        Return whether this path is a symlink. Always false (python/cpython#82102).
+        Return whether this path is a symlink.
         """
-        return False
+        info = self.root.getinfo(self.at)
+        mode = info.external_attr >> 16
+        return stat.S_ISLNK(mode)
 
     def glob(self, pattern):
         if not pattern:
diff --git a/Misc/NEWS.d/next/Library/2024-05-26-21-28-11.gh-issue-119588.wlLBK5.rst b/Misc/NEWS.d/next/Library/2024-05-26-21-28-11.gh-issue-119588.wlLBK5.rst
new file mode 100644 (file)
index 0000000..01321d8
--- /dev/null
@@ -0,0 +1 @@
+``zipfile.Path.is_symlink`` now assesses if the given path is a symlink.