Skip to content

Commit 34a6d89

Browse files
[3.13] gh-119588: Implement zipfile.Path.is_symlink (zipp 3.19.0). (GH-119591) (#119985)
gh-119588: Implement zipfile.Path.is_symlink (zipp 3.19.0). (GH-119591) (cherry picked from commit 42a34dd) Co-authored-by: Jason R. Coombs <[email protected]>
1 parent 23ebf87 commit 34a6d89

File tree

4 files changed

+32
-12
lines changed

4 files changed

+32
-12
lines changed

Doc/library/zipfile.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,15 @@ Path objects are traversable using the ``/`` operator or ``joinpath``.
585585

586586
Return ``True`` if the current context references a file.
587587

588+
.. method:: Path.is_symlink()
589+
590+
Return ``True`` if the current context references a symbolic link.
591+
592+
.. versionadded:: 3.12
593+
594+
.. versionchanged:: 3.12.4
595+
Prior to 3.12.4, ``is_symlink`` would unconditionally return ``False``.
596+
588597
.. method:: Path.exists()
589598

590599
Return ``True`` if the current context references a file or

Lib/test/test_zipfile/_path/test_path.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import contextlib
44
import pathlib
55
import pickle
6+
import stat
67
import sys
78
import unittest
89
import zipfile
@@ -21,12 +22,17 @@ class itertools:
2122
Counter = Counter
2223

2324

25+
def _make_link(info: zipfile.ZipInfo): # type: ignore[name-defined]
26+
info.external_attr |= stat.S_IFLNK << 16
27+
28+
2429
def build_alpharep_fixture():
2530
"""
2631
Create a zip file with this structure:
2732
2833
.
2934
├── a.txt
35+
├── n.txt (-> a.txt)
3036
├── b
3137
│ ├── c.txt
3238
│ ├── d
@@ -47,6 +53,7 @@ def build_alpharep_fixture():
4753
- multiple files in a directory (b/c, b/f)
4854
- a directory containing only a directory (g/h)
4955
- a directory with files of different extensions (j/klm)
56+
- a symlink (n) pointing to (a)
5057
5158
"alpha" because it uses alphabet
5259
"rep" because it's a representative example
@@ -61,6 +68,9 @@ def build_alpharep_fixture():
6168
zf.writestr("j/k.bin", b"content of k")
6269
zf.writestr("j/l.baz", b"content of l")
6370
zf.writestr("j/m.bar", b"content of m")
71+
zf.writestr("n.txt", b"a.txt")
72+
_make_link(zf.infolist()[-1])
73+
6474
zf.filename = "alpharep.zip"
6575
return zf
6676

@@ -91,7 +101,7 @@ def zipfile_ondisk(self, alpharep):
91101
def test_iterdir_and_types(self, alpharep):
92102
root = zipfile.Path(alpharep)
93103
assert root.is_dir()
94-
a, b, g, j = root.iterdir()
104+
a, k, b, g, j = root.iterdir()
95105
assert a.is_file()
96106
assert b.is_dir()
97107
assert g.is_dir()
@@ -111,7 +121,7 @@ def test_is_file_missing(self, alpharep):
111121
@pass_alpharep
112122
def test_iterdir_on_file(self, alpharep):
113123
root = zipfile.Path(alpharep)
114-
a, b, g, j = root.iterdir()
124+
a, k, b, g, j = root.iterdir()
115125
with self.assertRaises(ValueError):
116126
a.iterdir()
117127

@@ -126,7 +136,7 @@ def test_subdir_is_dir(self, alpharep):
126136
@pass_alpharep
127137
def test_open(self, alpharep):
128138
root = zipfile.Path(alpharep)
129-
a, b, g, j = root.iterdir()
139+
a, k, b, g, j = root.iterdir()
130140
with a.open(encoding="utf-8") as strm:
131141
data = strm.read()
132142
self.assertEqual(data, "content of a")
@@ -230,7 +240,7 @@ def test_open_missing_directory(self, alpharep):
230240
@pass_alpharep
231241
def test_read(self, alpharep):
232242
root = zipfile.Path(alpharep)
233-
a, b, g, j = root.iterdir()
243+
a, k, b, g, j = root.iterdir()
234244
assert a.read_text(encoding="utf-8") == "content of a"
235245
# Also check positional encoding arg (gh-101144).
236246
assert a.read_text("utf-8") == "content of a"
@@ -296,7 +306,7 @@ def test_mutability(self, alpharep):
296306
reflect that change.
297307
"""
298308
root = zipfile.Path(alpharep)
299-
a, b, g, j = root.iterdir()
309+
a, k, b, g, j = root.iterdir()
300310
alpharep.writestr('foo.txt', 'foo')
301311
alpharep.writestr('bar/baz.txt', 'baz')
302312
assert any(child.name == 'foo.txt' for child in root.iterdir())
@@ -513,12 +523,9 @@ def test_eq_hash(self, alpharep):
513523

514524
@pass_alpharep
515525
def test_is_symlink(self, alpharep):
516-
"""
517-
See python/cpython#82102 for symlink support beyond this object.
518-
"""
519-
520526
root = zipfile.Path(alpharep)
521-
assert not root.is_symlink()
527+
assert not root.joinpath('a.txt').is_symlink()
528+
assert root.joinpath('n.txt').is_symlink()
522529

523530
@pass_alpharep
524531
def test_relative_to(self, alpharep):

Lib/zipfile/_path/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import contextlib
66
import pathlib
77
import re
8+
import stat
89
import sys
910

1011
from .glob import Translator
@@ -390,9 +391,11 @@ def match(self, path_pattern):
390391

391392
def is_symlink(self):
392393
"""
393-
Return whether this path is a symlink. Always false (python/cpython#82102).
394+
Return whether this path is a symlink.
394395
"""
395-
return False
396+
info = self.root.getinfo(self.at)
397+
mode = info.external_attr >> 16
398+
return stat.S_ISLNK(mode)
396399

397400
def glob(self, pattern):
398401
if not pattern:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
``zipfile.Path.is_symlink`` now assesses if the given path is a symlink.

0 commit comments

Comments
 (0)