From c75ce49ae7f6c035dac79568344392f879e81ec5 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Wed, 2 Nov 2022 12:14:22 -0400 Subject: [PATCH 01/10] Teach zipp.Path some additional pathlib.Path methods. Covers: * Path.match * Path.glob * Path.rglob * Path.relative_to * Path.is_symlink (with trivial implementation given the lack of symlink support currently) along with an implementation of Path.__eq__ for testing/comparison purposes. Part of the implementation here makes use of CPython internals (from the pathlib module) but it seems to handle fixing that requires some refactoring upstream. --- tests/test_zipp.py | 38 +++++++++++++++++++++++++++++++++++ zipp/__init__.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/tests/test_zipp.py b/tests/test_zipp.py index cac2cde..0d30f5f 100644 --- a/tests/test_zipp.py +++ b/tests/test_zipp.py @@ -403,6 +403,44 @@ def test_root_unnamed(self, alpharep): assert sub.name == "b" assert sub.parent + @pass_alpharep + def test_match_and_glob(self, alpharep): + root = zipp.Path(alpharep) + assert not root.match("*.txt") + + assert list(root.glob("b/c.*")) == [zipp.Path(alpharep, "b/c.txt")] + + files = root.glob("**/*.txt") + assert all(each.match("*.txt") for each in files) + + assert list(root.glob("**/*.txt")) == list(root.rglob("*.txt")) + + @pass_alpharep + def test_eq_hash(self, alpharep): + root = zipp.Path(alpharep) + assert root == zipp.Path(alpharep) + + assert root != (root / "a.txt") + assert (root / "a.txt") == (root / "a.txt") + + root = zipp.Path(alpharep) + assert root in {root} + + @pass_alpharep + def test_is_symlink(self, alpharep): + """ + See python/cpython#82102 for symlink support beyond this object. + """ + + root = zipp.Path(alpharep) + assert not root.is_symlink() + + @pass_alpharep + def test_relative_to(self, alpharep): + root = zipp.Path(alpharep) + relative = root.joinpath("b", "c.txt").relative_to(root / "b") + assert relative == pathlib.Path("c.txt") + @pass_alpharep def test_inheritance(self, alpharep): cls = type('PathChild', (zipp.Path,), {}) diff --git a/zipp/__init__.py b/zipp/__init__.py index c1f3632..05e1058 100644 --- a/zipp/__init__.py +++ b/zipp/__init__.py @@ -1,4 +1,5 @@ import io +import os import posixpath import zipfile import itertools @@ -243,6 +244,14 @@ def __init__(self, root, at=""): self.root = FastLookup.make(root) self.at = at + def __eq__(self, other): + if self.__class__ is not other.__class__: + return NotImplemented + return (self.root, self.at) == (other.root, other.at) + + def __hash__(self): + return hash((self.root, self.at)) + def open(self, mode='r', *args, pwd=None, **kwargs): """ Open this entry as text or binary following the semantics @@ -313,6 +322,47 @@ def iterdir(self): subs = map(self._next, self.root.namelist()) return filter(self._is_child, subs) + def match(self, path_pattern): + return pathlib.Path(self.at).match(path_pattern) + + def is_symlink(self): + return False # See #82102 + + @contextlib.contextmanager + def _scandir(self): # Needed for glob + yield self.iterdir() + + def _make_child_relpath(self, relpath): # Needed for glob + return self / relpath + + def glob(self, pattern): + if not pattern: + raise ValueError("Unacceptable pattern: {!r}".format(pattern)) + path = pathlib.Path(self.at) + drv, root, pattern_parts = path._flavour.parse_parts((pattern,)) + if drv or root: + raise NotImplementedError("Non-relative patterns are unsupported") + if pattern[-1] in (path._flavour.sep, path._flavour.altsep): + pattern_parts.append('') + selector = pathlib._make_selector(tuple(pattern_parts), path._flavour) + for p in selector.select_from(self): + yield p + + def rglob(self, pattern): + path = pathlib.Path(self.at) + drv, root, pattern_parts = path._flavour.parse_parts((pattern,)) + if drv or root: + raise NotImplementedError("Non-relative patterns are unsupported") + if pattern and pattern[-1] in (path._flavour.sep, path._flavour.altsep): + pattern_parts.append('') + selector = pathlib._make_selector(("**",) + tuple(pattern_parts), path._flavour) + for p in selector.select_from(self): + yield p + + def relative_to(self, *other, **kwargs): + other = (pathlib.Path(each.at) for each in other) + return pathlib.Path(self.at).relative_to(*other, **kwargs) + def __str__(self): return posixpath.join(self.root.filename, self.at) From c678a4f340203619de0bd2f4832cc2fa7b6d356d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 25 Nov 2022 08:47:24 -0500 Subject: [PATCH 02/10] Remove unused import. --- zipp/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/zipp/__init__.py b/zipp/__init__.py index 05e1058..570d44d 100644 --- a/zipp/__init__.py +++ b/zipp/__init__.py @@ -1,5 +1,4 @@ import io -import os import posixpath import zipfile import itertools From e587f319826bce03df1dc7233842f054172c1fd2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 25 Nov 2022 10:55:16 -0500 Subject: [PATCH 03/10] Re-implement glob directly, adding compatibility for older Pythons and avoids platform-specific concerns. --- zipp/__init__.py | 39 +++++++++++++++------------------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/zipp/__init__.py b/zipp/__init__.py index 570d44d..1757e71 100644 --- a/zipp/__init__.py +++ b/zipp/__init__.py @@ -4,6 +4,8 @@ import itertools import contextlib import pathlib +import re +import fnmatch from .py310compat import text_encoding @@ -327,36 +329,25 @@ def match(self, path_pattern): def is_symlink(self): return False # See #82102 - @contextlib.contextmanager - def _scandir(self): # Needed for glob - yield self.iterdir() - - def _make_child_relpath(self, relpath): # Needed for glob - return self / relpath + def _descendants(self): + for child in self.iterdir(): + yield child + if child.is_dir(): + yield from child._descendants() def glob(self, pattern): if not pattern: raise ValueError("Unacceptable pattern: {!r}".format(pattern)) - path = pathlib.Path(self.at) - drv, root, pattern_parts = path._flavour.parse_parts((pattern,)) - if drv or root: - raise NotImplementedError("Non-relative patterns are unsupported") - if pattern[-1] in (path._flavour.sep, path._flavour.altsep): - pattern_parts.append('') - selector = pathlib._make_selector(tuple(pattern_parts), path._flavour) - for p in selector.select_from(self): - yield p + + matches = re.compile(fnmatch.translate(pattern)).fullmatch + return ( + child + for child in self._descendants() + if matches(str(child.relative_to(self))) + ) def rglob(self, pattern): - path = pathlib.Path(self.at) - drv, root, pattern_parts = path._flavour.parse_parts((pattern,)) - if drv or root: - raise NotImplementedError("Non-relative patterns are unsupported") - if pattern and pattern[-1] in (path._flavour.sep, path._flavour.altsep): - pattern_parts.append('') - selector = pathlib._make_selector(("**",) + tuple(pattern_parts), path._flavour) - for p in selector.select_from(self): - yield p + return self.glob(f'**/{pattern}') def relative_to(self, *other, **kwargs): other = (pathlib.Path(each.at) for each in other) From 83252516da213494307bc4fbd6191b49b255d8a0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 25 Nov 2022 11:00:36 -0500 Subject: [PATCH 04/10] Add docstring to __eq__, covering NotImplemented types. --- zipp/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/zipp/__init__.py b/zipp/__init__.py index 1757e71..f7fe44d 100644 --- a/zipp/__init__.py +++ b/zipp/__init__.py @@ -246,6 +246,10 @@ def __init__(self, root, at=""): self.at = at def __eq__(self, other): + """ + >>> Path(zipfile.ZipFile(io.BytesIO(), 'w')) == 'foo' + False + """ if self.__class__ is not other.__class__: return NotImplemented return (self.root, self.at) == (other.root, other.at) From 4e271ca8daa7f457b17fc1b83e78bf78d6d05b59 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 25 Nov 2022 11:02:51 -0500 Subject: [PATCH 05/10] Add test capturing empty glob pattern. --- tests/test_zipp.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_zipp.py b/tests/test_zipp.py index 0d30f5f..dfcd89d 100644 --- a/tests/test_zipp.py +++ b/tests/test_zipp.py @@ -415,6 +415,11 @@ def test_match_and_glob(self, alpharep): assert list(root.glob("**/*.txt")) == list(root.rglob("*.txt")) + def test_glob_empty(self): + root = zipp.Path(zipfile.ZipFile(io.BytesIO(), 'w')) + with self.assertRaises(ValueError): + root.glob('') + @pass_alpharep def test_eq_hash(self, alpharep): root = zipp.Path(alpharep) From 057ed578279915d4c7cdec3ebf7ad750bf0852aa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 25 Nov 2022 11:56:03 -0500 Subject: [PATCH 06/10] Use PurePosixPath for relative paths within a zip file. Fixes failing test on Windows. Added test for relative paths with subdirectories. --- tests/test_zipp.py | 5 ++++- zipp/__init__.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_zipp.py b/tests/test_zipp.py index dfcd89d..4208a83 100644 --- a/tests/test_zipp.py +++ b/tests/test_zipp.py @@ -444,7 +444,10 @@ def test_is_symlink(self, alpharep): def test_relative_to(self, alpharep): root = zipp.Path(alpharep) relative = root.joinpath("b", "c.txt").relative_to(root / "b") - assert relative == pathlib.Path("c.txt") + assert relative == pathlib.PurePosixPath("c.txt") + + relative = root.joinpath("b", "d", "e.txt").relative_to(root / "b") + assert str(relative) == "d/e.txt" @pass_alpharep def test_inheritance(self, alpharep): diff --git a/zipp/__init__.py b/zipp/__init__.py index f7fe44d..be56e4e 100644 --- a/zipp/__init__.py +++ b/zipp/__init__.py @@ -354,8 +354,8 @@ def rglob(self, pattern): return self.glob(f'**/{pattern}') def relative_to(self, *other, **kwargs): - other = (pathlib.Path(each.at) for each in other) - return pathlib.Path(self.at).relative_to(*other, **kwargs) + other = (pathlib.PurePosixPath(each.at) for each in other) + return pathlib.PurePosixPath(self.at).relative_to(*other, **kwargs) def __str__(self): return posixpath.join(self.root.filename, self.at) From e7f4ace9120f736fd56e2331267fcbe61c5dff3d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 25 Nov 2022 11:57:22 -0500 Subject: [PATCH 07/10] Remove constraint that the result of relative_to is a pathlib object. Instead, assert that its string value resolves to the expected value. --- tests/test_zipp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_zipp.py b/tests/test_zipp.py index 4208a83..1e17870 100644 --- a/tests/test_zipp.py +++ b/tests/test_zipp.py @@ -444,7 +444,7 @@ def test_is_symlink(self, alpharep): def test_relative_to(self, alpharep): root = zipp.Path(alpharep) relative = root.joinpath("b", "c.txt").relative_to(root / "b") - assert relative == pathlib.PurePosixPath("c.txt") + assert str(relative) == "c.txt" relative = root.joinpath("b", "d", "e.txt").relative_to(root / "b") assert str(relative) == "d/e.txt" From 25739bc9687674da5a9d211b41cfbb333e7f9a37 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 25 Nov 2022 12:09:28 -0500 Subject: [PATCH 08/10] Reimplement relative_to using posixpath primitives. --- zipp/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/zipp/__init__.py b/zipp/__init__.py index be56e4e..6f7133d 100644 --- a/zipp/__init__.py +++ b/zipp/__init__.py @@ -353,9 +353,8 @@ def glob(self, pattern): def rglob(self, pattern): return self.glob(f'**/{pattern}') - def relative_to(self, *other, **kwargs): - other = (pathlib.PurePosixPath(each.at) for each in other) - return pathlib.PurePosixPath(self.at).relative_to(*other, **kwargs) + 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) From 53e573bdb9b0ab4ff079069d2ceaa20c27fda445 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 25 Nov 2022 12:11:15 -0500 Subject: [PATCH 09/10] Move inline comment to docstring and expand org. --- zipp/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/zipp/__init__.py b/zipp/__init__.py index 6f7133d..ad01e27 100644 --- a/zipp/__init__.py +++ b/zipp/__init__.py @@ -331,7 +331,10 @@ def match(self, path_pattern): return pathlib.Path(self.at).match(path_pattern) def is_symlink(self): - return False # See #82102 + """ + Return whether this path is a symlink. Always false (python/cpython#82102). + """ + return False def _descendants(self): for child in self.iterdir(): From 7d052c11c5cebb8cd4906fba172168da00b890aa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 25 Nov 2022 13:04:12 -0500 Subject: [PATCH 10/10] Update changelog. --- CHANGES.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 3482d02..95c553f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,13 @@ +v3.11.0 +======= + +* #85: Added support for new methods on ``Path``: + + - ``match`` + - ``glob`` and ``rglob`` + - ``relative_to`` + - ``is_symlink`` + v3.10.0 =======