Skip to content

Commit c23d6fb

Browse files
committed
gh-101566: Sync with zipp 3.14.
1 parent 89413bb commit c23d6fb

File tree

6 files changed

+215
-56
lines changed

6 files changed

+215
-56
lines changed

Lib/test/test_zipfile/_context.py

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import contextlib
2+
import time
3+
4+
5+
class DeadlineExceeded(Exception):
6+
pass
7+
8+
9+
class TimedContext(contextlib.ContextDecorator):
10+
"""
11+
A context that will raise DeadlineExceeded if the
12+
max duration is reached during the execution.
13+
14+
>>> TimedContext(1)(time.sleep)(.1)
15+
>>> TimedContext(0)(time.sleep)(.1)
16+
Traceback (most recent call last):
17+
...
18+
tests._context.DeadlineExceeded: (..., 0)
19+
"""
20+
21+
def __init__(self, max_duration: int):
22+
self.max_duration = max_duration
23+
24+
def __enter__(self):
25+
self.start = time.monotonic()
26+
27+
def __exit__(self, *err):
28+
duration = time.monotonic() - self.start
29+
if duration > self.max_duration:
30+
raise DeadlineExceeded(duration, self.max_duration)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
try:
2+
from func_timeout import func_set_timeout as set_timeout
3+
except ImportError: # pragma: no cover
4+
# provide a fallback that doesn't actually time out
5+
from ._context import TimedContext as set_timeout
6+
7+
8+
__all__ = ['set_timeout']

Lib/test/test_zipfile/_itertools.py

+29
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,32 @@
1+
import itertools
2+
3+
4+
# from jaraco.itertools 6.3.0
5+
class Counter:
6+
"""
7+
Wrap an iterable in an object that stores the count of items
8+
that pass through it.
9+
10+
>>> items = Counter(range(20))
11+
>>> items.count
12+
0
13+
>>> values = list(items)
14+
>>> items.count
15+
20
16+
"""
17+
18+
def __init__(self, i):
19+
self.count = 0
20+
self.iter = zip(itertools.count(1), i)
21+
22+
def __iter__(self):
23+
return self
24+
25+
def __next__(self):
26+
self.count, result = next(self.iter)
27+
return result
28+
29+
130
# from more_itertools v8.13.0
231
def always_iterable(obj, base_type=(str, bytes)):
332
if obj is None:

Lib/test/test_zipfile/test_path.py

+84-53
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,25 @@
44
import pathlib
55
import pickle
66
import string
7-
from test.support.script_helper import assert_python_ok
7+
import sys
88
import unittest
99
import zipfile
1010

11-
from ._test_params import parameterize, Invoked
1211
from ._functools import compose
12+
from ._itertools import Counter
1313

14+
from ._test_params import parameterize, Invoked
15+
from ._func_timeout_compat import set_timeout
1416

1517
from test.support.os_helper import temp_dir
1618

1719

18-
# Poor man's technique to consume a (smallish) iterable.
19-
consume = tuple
20-
21-
22-
# from jaraco.itertools 5.0
2320
class jaraco:
2421
class itertools:
25-
class Counter:
26-
def __init__(self, i):
27-
self.count = 0
28-
self._orig_iter = iter(i)
22+
Counter = Counter
2923

30-
def __iter__(self):
31-
return self
3224

33-
def __next__(self):
34-
result = next(self._orig_iter)
35-
self.count += 1
36-
return result
25+
consume = tuple
3726

3827

3928
def add_dirs(zf):
@@ -161,10 +150,10 @@ def test_open_encoding_utf16(self):
161150
u16 = path.joinpath("16.txt")
162151
with u16.open('r', "utf-16") as strm:
163152
data = strm.read()
164-
self.assertEqual(data, "This was utf-16")
153+
assert data == "This was utf-16"
165154
with u16.open(encoding="utf-16") as strm:
166155
data = strm.read()
167-
self.assertEqual(data, "This was utf-16")
156+
assert data == "This was utf-16"
168157

169158
def test_open_encoding_errors(self):
170159
in_memory_file = io.BytesIO()
@@ -177,9 +166,9 @@ def test_open_encoding_errors(self):
177166

178167
# encoding= as a positional argument for gh-101144.
179168
data = u16.read_text("utf-8", errors="ignore")
180-
self.assertEqual(data, "invalid utf-8: .")
169+
assert data == "invalid utf-8: ."
181170
with u16.open("r", "utf-8", errors="surrogateescape") as f:
182-
self.assertEqual(f.read(), "invalid utf-8: \udcff\udcff.")
171+
assert f.read() == "invalid utf-8: \udcff\udcff."
183172

184173
# encoding= both positional and keyword is an error; gh-101144.
185174
with self.assertRaisesRegex(TypeError, "encoding"):
@@ -191,24 +180,21 @@ def test_open_encoding_errors(self):
191180
with self.assertRaises(UnicodeDecodeError):
192181
f.read()
193182

194-
def test_encoding_warnings(self):
183+
@unittest.skipIf(
184+
not getattr(sys.flags, 'warn_default_encoding', 0),
185+
"Requires warn_default_encoding",
186+
)
187+
@pass_alpharep
188+
def test_encoding_warnings(self, alpharep):
195189
"""EncodingWarning must blame the read_text and open calls."""
196-
code = '''\
197-
import io, zipfile
198-
with zipfile.ZipFile(io.BytesIO(), "w") as zf:
199-
zf.filename = '<test_encoding_warnings in memory zip file>'
200-
zf.writestr("path/file.txt", b"Spanish Inquisition")
201-
root = zipfile.Path(zf)
202-
(path,) = root.iterdir()
203-
file_path = path.joinpath("file.txt")
204-
unused = file_path.read_text() # should warn
205-
file_path.open("r").close() # should warn
206-
'''
207-
proc = assert_python_ok('-X', 'warn_default_encoding', '-c', code)
208-
warnings = proc.err.splitlines()
209-
self.assertEqual(len(warnings), 2, proc.err)
210-
self.assertRegex(warnings[0], rb"^<string>:8: EncodingWarning:")
211-
self.assertRegex(warnings[1], rb"^<string>:9: EncodingWarning:")
190+
assert sys.flags.warn_default_encoding
191+
root = zipfile.Path(alpharep)
192+
with self.assertWarns(EncodingWarning) as wc:
193+
root.joinpath("a.txt").read_text()
194+
assert __file__ == wc.filename
195+
with self.assertWarns(EncodingWarning) as wc:
196+
root.joinpath("a.txt").open("r").close()
197+
assert __file__ == wc.filename
212198

213199
def test_open_write(self):
214200
"""
@@ -250,7 +236,8 @@ def test_read(self, alpharep):
250236
root = zipfile.Path(alpharep)
251237
a, b, g = root.iterdir()
252238
assert a.read_text(encoding="utf-8") == "content of a"
253-
a.read_text("utf-8") # No positional arg TypeError per gh-101144.
239+
# Also check positional encoding arg (gh-101144).
240+
assert a.read_text("utf-8") == "content of a"
254241
assert a.read_bytes() == b"content of a"
255242

256243
@pass_alpharep
@@ -275,19 +262,6 @@ def test_traverse_truediv(self, alpharep):
275262
e = root / "b" / "d" / "e.txt"
276263
assert e.read_text(encoding="utf-8") == "content of e"
277264

278-
@pass_alpharep
279-
def test_traverse_simplediv(self, alpharep):
280-
"""
281-
Disable the __future__.division when testing traversal.
282-
"""
283-
code = compile(
284-
source="zipfile.Path(alpharep) / 'a'",
285-
filename="(test)",
286-
mode="eval",
287-
dont_inherit=True,
288-
)
289-
eval(code)
290-
291265
@pass_alpharep
292266
def test_pathlike_construction(self, alpharep):
293267
"""
@@ -356,7 +330,7 @@ def test_joinpath_constant_time(self):
356330
# Check the file iterated all items
357331
assert entries.count == self.HUGE_ZIPFILE_NUM_ENTRIES
358332

359-
# @func_timeout.func_set_timeout(3)
333+
@set_timeout(3)
360334
def test_implied_dirs_performance(self):
361335
data = ['/'.join(string.ascii_lowercase + str(n)) for n in range(10000)]
362336
zipfile.CompleteDirs._implied_dirs(data)
@@ -472,6 +446,52 @@ def test_root_unnamed(self, alpharep):
472446
assert sub.name == "b"
473447
assert sub.parent
474448

449+
@pass_alpharep
450+
def test_match_and_glob(self, alpharep):
451+
root = zipfile.Path(alpharep)
452+
assert not root.match("*.txt")
453+
454+
assert list(root.glob("b/c.*")) == [zipfile.Path(alpharep, "b/c.txt")]
455+
456+
files = root.glob("**/*.txt")
457+
assert all(each.match("*.txt") for each in files)
458+
459+
assert list(root.glob("**/*.txt")) == list(root.rglob("*.txt"))
460+
461+
def test_glob_empty(self):
462+
root = zipfile.Path(zipfile.ZipFile(io.BytesIO(), 'w'))
463+
with self.assertRaises(ValueError):
464+
root.glob('')
465+
466+
@pass_alpharep
467+
def test_eq_hash(self, alpharep):
468+
root = zipfile.Path(alpharep)
469+
assert root == zipfile.Path(alpharep)
470+
471+
assert root != (root / "a.txt")
472+
assert (root / "a.txt") == (root / "a.txt")
473+
474+
root = zipfile.Path(alpharep)
475+
assert root in {root}
476+
477+
@pass_alpharep
478+
def test_is_symlink(self, alpharep):
479+
"""
480+
See python/cpython#82102 for symlink support beyond this object.
481+
"""
482+
483+
root = zipfile.Path(alpharep)
484+
assert not root.is_symlink()
485+
486+
@pass_alpharep
487+
def test_relative_to(self, alpharep):
488+
root = zipfile.Path(alpharep)
489+
relative = root.joinpath("b", "c.txt").relative_to(root / "b")
490+
assert str(relative) == "c.txt"
491+
492+
relative = root.joinpath("b", "d", "e.txt").relative_to(root / "b")
493+
assert str(relative) == "d/e.txt"
494+
475495
@pass_alpharep
476496
def test_inheritance(self, alpharep):
477497
cls = type('PathChild', (zipfile.Path,), {})
@@ -493,3 +513,14 @@ def test_pickle(self, alpharep, path_type, subpath):
493513
restored_1 = pickle.loads(saved_1)
494514
first, *rest = restored_1.iterdir()
495515
assert first.read_text().startswith('content of ')
516+
517+
@pass_alpharep
518+
def test_extract_orig_with_implied_dirs(self, alpharep):
519+
"""
520+
A zip file wrapped in a Path should extract even with implied dirs.
521+
"""
522+
source_path = self.zipfile_ondisk(alpharep)
523+
zf = zipfile.ZipFile(source_path)
524+
# wrap the zipfile for its side effect
525+
zipfile.Path(zf)
526+
zf.extractall(source_path.parent)

Lib/zipfile/_path.py

+60-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import itertools
55
import contextlib
66
import pathlib
7+
import re
8+
import fnmatch
79

810

911
__all__ = ['Path']
@@ -93,7 +95,7 @@ def _implied_dirs(names):
9395
return _dedupe(_difference(as_dirs, names))
9496

9597
def namelist(self):
96-
names = super(CompleteDirs, self).namelist()
98+
names = super().namelist()
9799
return names + list(self._implied_dirs(names))
98100

99101
def _name_set(self):
@@ -109,6 +111,17 @@ def resolve_dir(self, name):
109111
dir_match = name not in names and dirname in names
110112
return dirname if dir_match else name
111113

114+
def getinfo(self, name):
115+
"""
116+
Supplement getinfo for implied dirs.
117+
"""
118+
try:
119+
return super().getinfo(name)
120+
except KeyError:
121+
if not name.endswith('/') or name not in self._name_set():
122+
raise
123+
return zipfile.ZipInfo(filename=name)
124+
112125
@classmethod
113126
def make(cls, source):
114127
"""
@@ -138,13 +151,13 @@ class FastLookup(CompleteDirs):
138151
def namelist(self):
139152
with contextlib.suppress(AttributeError):
140153
return self.__names
141-
self.__names = super(FastLookup, self).namelist()
154+
self.__names = super().namelist()
142155
return self.__names
143156

144157
def _name_set(self):
145158
with contextlib.suppress(AttributeError):
146159
return self.__lookup
147-
self.__lookup = super(FastLookup, self)._name_set()
160+
self.__lookup = super()._name_set()
148161
return self.__lookup
149162

150163

@@ -246,6 +259,18 @@ def __init__(self, root, at=""):
246259
self.root = FastLookup.make(root)
247260
self.at = at
248261

262+
def __eq__(self, other):
263+
"""
264+
>>> Path(zipfile.ZipFile(io.BytesIO(), 'w')) == 'foo'
265+
False
266+
"""
267+
if self.__class__ is not other.__class__:
268+
return NotImplemented
269+
return (self.root, self.at) == (other.root, other.at)
270+
271+
def __hash__(self):
272+
return hash((self.root, self.at))
273+
249274
def open(self, mode='r', *args, pwd=None, **kwargs):
250275
"""
251276
Open this entry as text or binary following the semantics
@@ -316,6 +341,38 @@ def iterdir(self):
316341
subs = map(self._next, self.root.namelist())
317342
return filter(self._is_child, subs)
318343

344+
def match(self, path_pattern):
345+
return pathlib.Path(self.at).match(path_pattern)
346+
347+
def is_symlink(self):
348+
"""
349+
Return whether this path is a symlink. Always false (python/cpython#82102).
350+
"""
351+
return False
352+
353+
def _descendants(self):
354+
for child in self.iterdir():
355+
yield child
356+
if child.is_dir():
357+
yield from child._descendants()
358+
359+
def glob(self, pattern):
360+
if not pattern:
361+
raise ValueError(f"Unacceptable pattern: {pattern!r}")
362+
363+
matches = re.compile(fnmatch.translate(pattern)).fullmatch
364+
return (
365+
child
366+
for child in self._descendants()
367+
if matches(str(child.relative_to(self)))
368+
)
369+
370+
def rglob(self, pattern):
371+
return self.glob(f'**/{pattern}')
372+
373+
def relative_to(self, other, *extra):
374+
return posixpath.relpath(str(self), str(other.joinpath(*extra)))
375+
319376
def __str__(self):
320377
return posixpath.join(self.root.filename, self.at)
321378

0 commit comments

Comments
 (0)