]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
GH-127381: pathlib ABCs: remove `PathBase.resolve()` and `absolute()` (#127707)
authorBarney Gale <barney.gale@gmail.com>
Fri, 6 Dec 2024 21:39:45 +0000 (21:39 +0000)
committerGitHub <noreply@github.com>
Fri, 6 Dec 2024 21:39:45 +0000 (21:39 +0000)
Remove our implementation of POSIX path resolution in `PathBase.resolve()`.
This functionality is rather fragile and isn't necessary in most cases. It
depends on `PathBase.stat()`, which we're looking to remove.

Also remove `PathBase.absolute()`. Many legitimate virtual filesystems lack
the notion of a 'current directory', so it's wrong to include in the basic
interface.

Lib/pathlib/_abc.py
Lib/test/test_pathlib/test_pathlib.py
Lib/test/test_pathlib/test_pathlib_abc.py

index 11a11ecc4c820373604a4ef6c22129ba893e1da7..820970fcd5889b83d811dd5fb949a9db65f5c821 100644 (file)
@@ -13,7 +13,6 @@ resemble pathlib's PurePath and Path respectively.
 
 import functools
 import operator
-import posixpath
 from errno import EINVAL
 from glob import _GlobberBase, _no_recurse_symlinks
 from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO
@@ -115,11 +114,6 @@ class PurePathBase:
         # The `_raw_paths` slot stores unjoined string paths. This is set in
         # the `__init__()` method.
         '_raw_paths',
-
-        # The '_resolving' slot stores a boolean indicating whether the path
-        # is being processed by `PathBase.resolve()`. This prevents duplicate
-        # work from occurring when `resolve()` calls `stat()` or `readlink()`.
-        '_resolving',
     )
     parser = ParserBase()
     _globber = PathGlobber
@@ -130,7 +124,6 @@ class PurePathBase:
                 raise TypeError(
                     f"argument should be a str, not {type(arg).__name__!r}")
         self._raw_paths = list(args)
-        self._resolving = False
 
     def with_segments(self, *pathsegments):
         """Construct a new path object from any number of path-like objects.
@@ -339,9 +332,7 @@ class PurePathBase:
         path = str(self)
         parent = self.parser.split(path)[0]
         if path != parent:
-            parent = self.with_segments(parent)
-            parent._resolving = self._resolving
-            return parent
+            return self.with_segments(parent)
         return self
 
     @property
@@ -424,9 +415,6 @@ class PathBase(PurePathBase):
     """
     __slots__ = ()
 
-    # Maximum number of symlinks to follow in resolve()
-    _max_symlinks = 40
-
     @classmethod
     def _unsupported_msg(cls, attribute):
         return f"{cls.__name__}.{attribute} is unsupported"
@@ -720,20 +708,6 @@ class PathBase(PurePathBase):
                 yield path, dirnames, filenames
                 paths += [path.joinpath(d) for d in reversed(dirnames)]
 
-    def absolute(self):
-        """Return an absolute version of this path
-        No normalization or symlink resolution is performed.
-
-        Use resolve() to resolve symlinks and remove '..' segments.
-        """
-        if self.is_absolute():
-            return self
-        elif self.parser is not posixpath:
-            raise UnsupportedOperation(self._unsupported_msg('absolute()'))
-        else:
-            # Treat the root directory as the current working directory.
-            return self.with_segments('/', *self._raw_paths)
-
     def expanduser(self):
         """ Return a new path with expanded ~ and ~user constructs
         (as returned by os.path.expanduser)
@@ -745,42 +719,6 @@ class PathBase(PurePathBase):
         Return the path to which the symbolic link points.
         """
         raise UnsupportedOperation(self._unsupported_msg('readlink()'))
-    readlink._supported = False
-
-    def resolve(self, strict=False):
-        """
-        Make the path absolute, resolving all symlinks on the way and also
-        normalizing it.
-        """
-        if self._resolving:
-            return self
-        elif self.parser is not posixpath:
-            raise UnsupportedOperation(self._unsupported_msg('resolve()'))
-
-        def raise_error(*args):
-            raise OSError("Unsupported operation.")
-
-        getcwd = raise_error
-        if strict or getattr(self.readlink, '_supported', True):
-            def lstat(path_str):
-                path = self.with_segments(path_str)
-                path._resolving = True
-                return path.stat(follow_symlinks=False)
-
-            def readlink(path_str):
-                path = self.with_segments(path_str)
-                path._resolving = True
-                return str(path.readlink())
-        else:
-            # If the user has *not* overridden the `readlink()` method, then
-            # symlinks are unsupported and (in non-strict mode) we can improve
-            # performance by not calling `path.lstat()`.
-            lstat = readlink = raise_error
-
-        return self.with_segments(posixpath._realpath(
-            str(self.absolute()), strict, self.parser.sep,
-            getcwd=getcwd, lstat=lstat, readlink=readlink,
-            maxlinks=self._max_symlinks))
 
     def symlink_to(self, target, target_is_directory=False):
         """
index 2c48eeeda145d0d33c832eef16df92364559d566..8c9049f15d5bf9a75d068d4fbadf5580983139e5 100644 (file)
@@ -1,3 +1,4 @@
+import collections
 import contextlib
 import io
 import os
@@ -21,7 +22,7 @@ from test.support import swap_attr
 from test.support import os_helper
 from test.support.os_helper import TESTFN, FakePath
 from test.test_pathlib import test_pathlib_abc
-from test.test_pathlib.test_pathlib_abc import needs_posix, needs_windows, needs_symlinks
+from test.test_pathlib.test_pathlib_abc import needs_posix, needs_windows
 
 try:
     import fcntl
@@ -55,6 +56,13 @@ def patch_replace(old_test):
             self.cls.replace = old_replace
     return new_test
 
+
+_tests_needing_symlinks = set()
+def needs_symlinks(fn):
+    """Decorator that marks a test as requiring a path class that supports symlinks."""
+    _tests_needing_symlinks.add(fn.__name__)
+    return fn
+
 #
 # Tests for the pure classes.
 #
@@ -533,6 +541,9 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
     can_symlink = os_helper.can_symlink()
 
     def setUp(self):
+        name = self.id().split('.')[-1]
+        if name in _tests_needing_symlinks and not self.can_symlink:
+            self.skipTest('requires symlinks')
         super().setUp()
         os.chmod(self.parser.join(self.base, 'dirE'), 0)
 
@@ -693,6 +704,34 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
         if hasattr(source_st, 'st_flags'):
             self.assertEqual(source_st.st_flags, target_st.st_flags)
 
+    @needs_symlinks
+    def test_copy_file_to_existing_symlink(self):
+        base = self.cls(self.base)
+        source = base / 'dirB' / 'fileB'
+        target = base / 'linkA'
+        real_target = base / 'fileA'
+        result = source.copy(target)
+        self.assertEqual(result, target)
+        self.assertTrue(target.exists())
+        self.assertTrue(target.is_symlink())
+        self.assertTrue(real_target.exists())
+        self.assertFalse(real_target.is_symlink())
+        self.assertEqual(source.read_text(), real_target.read_text())
+
+    @needs_symlinks
+    def test_copy_file_to_existing_symlink_follow_symlinks_false(self):
+        base = self.cls(self.base)
+        source = base / 'dirB' / 'fileB'
+        target = base / 'linkA'
+        real_target = base / 'fileA'
+        result = source.copy(target, follow_symlinks=False)
+        self.assertEqual(result, target)
+        self.assertTrue(target.exists())
+        self.assertTrue(target.is_symlink())
+        self.assertTrue(real_target.exists())
+        self.assertFalse(real_target.is_symlink())
+        self.assertEqual(source.read_text(), real_target.read_text())
+
     @os_helper.skip_unless_xattr
     def test_copy_file_preserve_metadata_xattrs(self):
         base = self.cls(self.base)
@@ -702,6 +741,118 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
         source.copy(target, preserve_metadata=True)
         self.assertEqual(os.getxattr(target, b'user.foo'), b'42')
 
+    @needs_symlinks
+    def test_copy_symlink_follow_symlinks_true(self):
+        base = self.cls(self.base)
+        source = base / 'linkA'
+        target = base / 'copyA'
+        result = source.copy(target)
+        self.assertEqual(result, target)
+        self.assertTrue(target.exists())
+        self.assertFalse(target.is_symlink())
+        self.assertEqual(source.read_text(), target.read_text())
+
+    @needs_symlinks
+    def test_copy_symlink_follow_symlinks_false(self):
+        base = self.cls(self.base)
+        source = base / 'linkA'
+        target = base / 'copyA'
+        result = source.copy(target, follow_symlinks=False)
+        self.assertEqual(result, target)
+        self.assertTrue(target.exists())
+        self.assertTrue(target.is_symlink())
+        self.assertEqual(source.readlink(), target.readlink())
+
+    @needs_symlinks
+    def test_copy_symlink_to_itself(self):
+        base = self.cls(self.base)
+        source = base / 'linkA'
+        self.assertRaises(OSError, source.copy, source)
+
+    @needs_symlinks
+    def test_copy_symlink_to_existing_symlink(self):
+        base = self.cls(self.base)
+        source = base / 'copySource'
+        target = base / 'copyTarget'
+        source.symlink_to(base / 'fileA')
+        target.symlink_to(base / 'dirC')
+        self.assertRaises(OSError, source.copy, target)
+        self.assertRaises(OSError, source.copy, target, follow_symlinks=False)
+
+    @needs_symlinks
+    def test_copy_symlink_to_existing_directory_symlink(self):
+        base = self.cls(self.base)
+        source = base / 'copySource'
+        target = base / 'copyTarget'
+        source.symlink_to(base / 'fileA')
+        target.symlink_to(base / 'dirC')
+        self.assertRaises(OSError, source.copy, target)
+        self.assertRaises(OSError, source.copy, target, follow_symlinks=False)
+
+    @needs_symlinks
+    def test_copy_directory_symlink_follow_symlinks_false(self):
+        base = self.cls(self.base)
+        source = base / 'linkB'
+        target = base / 'copyA'
+        result = source.copy(target, follow_symlinks=False)
+        self.assertEqual(result, target)
+        self.assertTrue(target.exists())
+        self.assertTrue(target.is_symlink())
+        self.assertEqual(source.readlink(), target.readlink())
+
+    @needs_symlinks
+    def test_copy_directory_symlink_to_itself(self):
+        base = self.cls(self.base)
+        source = base / 'linkB'
+        self.assertRaises(OSError, source.copy, source)
+        self.assertRaises(OSError, source.copy, source, follow_symlinks=False)
+
+    @needs_symlinks
+    def test_copy_directory_symlink_into_itself(self):
+        base = self.cls(self.base)
+        source = base / 'linkB'
+        target = base / 'linkB' / 'copyB'
+        self.assertRaises(OSError, source.copy, target)
+        self.assertRaises(OSError, source.copy, target, follow_symlinks=False)
+        self.assertFalse(target.exists())
+
+    @needs_symlinks
+    def test_copy_directory_symlink_to_existing_symlink(self):
+        base = self.cls(self.base)
+        source = base / 'copySource'
+        target = base / 'copyTarget'
+        source.symlink_to(base / 'dirC')
+        target.symlink_to(base / 'fileA')
+        self.assertRaises(FileExistsError, source.copy, target)
+        self.assertRaises(FileExistsError, source.copy, target, follow_symlinks=False)
+
+    @needs_symlinks
+    def test_copy_directory_symlink_to_existing_directory_symlink(self):
+        base = self.cls(self.base)
+        source = base / 'copySource'
+        target = base / 'copyTarget'
+        source.symlink_to(base / 'dirC' / 'dirD')
+        target.symlink_to(base / 'dirC')
+        self.assertRaises(FileExistsError, source.copy, target)
+        self.assertRaises(FileExistsError, source.copy, target, follow_symlinks=False)
+
+    @needs_symlinks
+    def test_copy_dangling_symlink(self):
+        base = self.cls(self.base)
+        source = base / 'source'
+        target = base / 'target'
+
+        source.mkdir()
+        source.joinpath('link').symlink_to('nonexistent')
+
+        self.assertRaises(FileNotFoundError, source.copy, target)
+
+        target2 = base / 'target2'
+        result = source.copy(target2, follow_symlinks=False)
+        self.assertEqual(result, target2)
+        self.assertTrue(target2.joinpath('link').is_symlink())
+        self.assertEqual(target2.joinpath('link').readlink(), self.cls('nonexistent'))
+
     @needs_symlinks
     def test_copy_link_preserve_metadata(self):
         base = self.cls(self.base)
@@ -801,6 +952,54 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
         target_file = target.joinpath('dirD', 'fileD')
         self.assertEqual(os.getxattr(target_file, b'user.foo'), b'42')
 
+    @needs_symlinks
+    def test_move_file_symlink(self):
+        base = self.cls(self.base)
+        source = base / 'linkA'
+        source_readlink = source.readlink()
+        target = base / 'linkA_moved'
+        result = source.move(target)
+        self.assertEqual(result, target)
+        self.assertFalse(source.exists())
+        self.assertTrue(target.is_symlink())
+        self.assertEqual(source_readlink, target.readlink())
+
+    @needs_symlinks
+    def test_move_file_symlink_to_itself(self):
+        base = self.cls(self.base)
+        source = base / 'linkA'
+        self.assertRaises(OSError, source.move, source)
+
+    @needs_symlinks
+    def test_move_dir_symlink(self):
+        base = self.cls(self.base)
+        source = base / 'linkB'
+        source_readlink = source.readlink()
+        target = base / 'linkB_moved'
+        result = source.move(target)
+        self.assertEqual(result, target)
+        self.assertFalse(source.exists())
+        self.assertTrue(target.is_symlink())
+        self.assertEqual(source_readlink, target.readlink())
+
+    @needs_symlinks
+    def test_move_dir_symlink_to_itself(self):
+        base = self.cls(self.base)
+        source = base / 'linkB'
+        self.assertRaises(OSError, source.move, source)
+
+    @needs_symlinks
+    def test_move_dangling_symlink(self):
+        base = self.cls(self.base)
+        source = base / 'brokenLink'
+        source_readlink = source.readlink()
+        target = base / 'brokenLink_moved'
+        result = source.move(target)
+        self.assertEqual(result, target)
+        self.assertFalse(source.exists())
+        self.assertTrue(target.is_symlink())
+        self.assertEqual(source_readlink, target.readlink())
+
     @patch_replace
     def test_move_file_other_fs(self):
         self.test_move_file()
@@ -858,9 +1057,41 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
     def test_move_into_empty_name_other_os(self):
         self.test_move_into_empty_name()
 
+    @needs_symlinks
+    def test_complex_symlinks_absolute(self):
+        self._check_complex_symlinks(self.base)
+
+    @needs_symlinks
+    def test_complex_symlinks_relative(self):
+        self._check_complex_symlinks('.')
+
+    @needs_symlinks
+    def test_complex_symlinks_relative_dot_dot(self):
+        self._check_complex_symlinks(self.parser.join('dirA', '..'))
+
     def _check_complex_symlinks(self, link0_target):
-        super()._check_complex_symlinks(link0_target)
+        # Test solving a non-looping chain of symlinks (issue #19887).
+        parser = self.parser
         P = self.cls(self.base)
+        P.joinpath('link1').symlink_to(parser.join('link0', 'link0'), target_is_directory=True)
+        P.joinpath('link2').symlink_to(parser.join('link1', 'link1'), target_is_directory=True)
+        P.joinpath('link3').symlink_to(parser.join('link2', 'link2'), target_is_directory=True)
+        P.joinpath('link0').symlink_to(link0_target, target_is_directory=True)
+
+        # Resolve absolute paths.
+        p = (P / 'link0').resolve()
+        self.assertEqual(p, P)
+        self.assertEqualNormCase(str(p), self.base)
+        p = (P / 'link1').resolve()
+        self.assertEqual(p, P)
+        self.assertEqualNormCase(str(p), self.base)
+        p = (P / 'link2').resolve()
+        self.assertEqual(p, P)
+        self.assertEqualNormCase(str(p), self.base)
+        p = (P / 'link3').resolve()
+        self.assertEqual(p, P)
+        self.assertEqualNormCase(str(p), self.base)
+
         # Resolve relative paths.
         old_path = os.getcwd()
         os.chdir(self.base)
@@ -880,6 +1111,118 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
         finally:
             os.chdir(old_path)
 
+    def _check_resolve(self, p, expected, strict=True):
+        q = p.resolve(strict)
+        self.assertEqual(q, expected)
+
+    # This can be used to check both relative and absolute resolutions.
+    _check_resolve_relative = _check_resolve_absolute = _check_resolve
+
+    @needs_symlinks
+    def test_resolve_common(self):
+        P = self.cls
+        p = P(self.base, 'foo')
+        with self.assertRaises(OSError) as cm:
+            p.resolve(strict=True)
+        self.assertEqual(cm.exception.errno, errno.ENOENT)
+        # Non-strict
+        parser = self.parser
+        self.assertEqualNormCase(str(p.resolve(strict=False)),
+                                 parser.join(self.base, 'foo'))
+        p = P(self.base, 'foo', 'in', 'spam')
+        self.assertEqualNormCase(str(p.resolve(strict=False)),
+                                 parser.join(self.base, 'foo', 'in', 'spam'))
+        p = P(self.base, '..', 'foo', 'in', 'spam')
+        self.assertEqualNormCase(str(p.resolve(strict=False)),
+                                 parser.join(parser.dirname(self.base), 'foo', 'in', 'spam'))
+        # These are all relative symlinks.
+        p = P(self.base, 'dirB', 'fileB')
+        self._check_resolve_relative(p, p)
+        p = P(self.base, 'linkA')
+        self._check_resolve_relative(p, P(self.base, 'fileA'))
+        p = P(self.base, 'dirA', 'linkC', 'fileB')
+        self._check_resolve_relative(p, P(self.base, 'dirB', 'fileB'))
+        p = P(self.base, 'dirB', 'linkD', 'fileB')
+        self._check_resolve_relative(p, P(self.base, 'dirB', 'fileB'))
+        # Non-strict
+        p = P(self.base, 'dirA', 'linkC', 'fileB', 'foo', 'in', 'spam')
+        self._check_resolve_relative(p, P(self.base, 'dirB', 'fileB', 'foo', 'in',
+                                          'spam'), False)
+        p = P(self.base, 'dirA', 'linkC', '..', 'foo', 'in', 'spam')
+        if self.cls.parser is not posixpath:
+            # In Windows, if linkY points to dirB, 'dirA\linkY\..'
+            # resolves to 'dirA' without resolving linkY first.
+            self._check_resolve_relative(p, P(self.base, 'dirA', 'foo', 'in',
+                                              'spam'), False)
+        else:
+            # In Posix, if linkY points to dirB, 'dirA/linkY/..'
+            # resolves to 'dirB/..' first before resolving to parent of dirB.
+            self._check_resolve_relative(p, P(self.base, 'foo', 'in', 'spam'), False)
+        # Now create absolute symlinks.
+        d = self.tempdir()
+        P(self.base, 'dirA', 'linkX').symlink_to(d)
+        P(self.base, str(d), 'linkY').symlink_to(self.parser.join(self.base, 'dirB'))
+        p = P(self.base, 'dirA', 'linkX', 'linkY', 'fileB')
+        self._check_resolve_absolute(p, P(self.base, 'dirB', 'fileB'))
+        # Non-strict
+        p = P(self.base, 'dirA', 'linkX', 'linkY', 'foo', 'in', 'spam')
+        self._check_resolve_relative(p, P(self.base, 'dirB', 'foo', 'in', 'spam'),
+                                     False)
+        p = P(self.base, 'dirA', 'linkX', 'linkY', '..', 'foo', 'in', 'spam')
+        if self.cls.parser is not posixpath:
+            # In Windows, if linkY points to dirB, 'dirA\linkY\..'
+            # resolves to 'dirA' without resolving linkY first.
+            self._check_resolve_relative(p, P(d, 'foo', 'in', 'spam'), False)
+        else:
+            # In Posix, if linkY points to dirB, 'dirA/linkY/..'
+            # resolves to 'dirB/..' first before resolving to parent of dirB.
+            self._check_resolve_relative(p, P(self.base, 'foo', 'in', 'spam'), False)
+
+    @needs_symlinks
+    def test_resolve_dot(self):
+        # See http://web.archive.org/web/20200623062557/https://bitbucket.org/pitrou/pathlib/issues/9/
+        parser = self.parser
+        p = self.cls(self.base)
+        p.joinpath('0').symlink_to('.', target_is_directory=True)
+        p.joinpath('1').symlink_to(parser.join('0', '0'), target_is_directory=True)
+        p.joinpath('2').symlink_to(parser.join('1', '1'), target_is_directory=True)
+        q = p / '2'
+        self.assertEqual(q.resolve(strict=True), p)
+        r = q / '3' / '4'
+        self.assertRaises(FileNotFoundError, r.resolve, strict=True)
+        # Non-strict
+        self.assertEqual(r.resolve(strict=False), p / '3' / '4')
+
+    def _check_symlink_loop(self, *args):
+        path = self.cls(*args)
+        with self.assertRaises(OSError) as cm:
+            path.resolve(strict=True)
+        self.assertEqual(cm.exception.errno, errno.ELOOP)
+
+    @needs_posix
+    @needs_symlinks
+    def test_resolve_loop(self):
+        # Loops with relative symlinks.
+        self.cls(self.base, 'linkX').symlink_to('linkX/inside')
+        self._check_symlink_loop(self.base, 'linkX')
+        self.cls(self.base, 'linkY').symlink_to('linkY')
+        self._check_symlink_loop(self.base, 'linkY')
+        self.cls(self.base, 'linkZ').symlink_to('linkZ/../linkZ')
+        self._check_symlink_loop(self.base, 'linkZ')
+        # Non-strict
+        p = self.cls(self.base, 'linkZ', 'foo')
+        self.assertEqual(p.resolve(strict=False), p)
+        # Loops with absolute symlinks.
+        self.cls(self.base, 'linkU').symlink_to(self.parser.join(self.base, 'linkU/inside'))
+        self._check_symlink_loop(self.base, 'linkU')
+        self.cls(self.base, 'linkV').symlink_to(self.parser.join(self.base, 'linkV'))
+        self._check_symlink_loop(self.base, 'linkV')
+        self.cls(self.base, 'linkW').symlink_to(self.parser.join(self.base, 'linkW/../linkW'))
+        self._check_symlink_loop(self.base, 'linkW')
+        # Non-strict
+        q = self.cls(self.base, 'linkW', 'foo')
+        self.assertEqual(q.resolve(strict=False), q)
+
     def test_resolve_nonexist_relative_issue38671(self):
         p = self.cls('non', 'exist')
 
@@ -890,6 +1233,24 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
         finally:
             os.chdir(old_cwd)
 
+    @needs_symlinks
+    def test_readlink(self):
+        P = self.cls(self.base)
+        self.assertEqual((P / 'linkA').readlink(), self.cls('fileA'))
+        self.assertEqual((P / 'brokenLink').readlink(),
+                         self.cls('non-existing'))
+        self.assertEqual((P / 'linkB').readlink(), self.cls('dirB'))
+        self.assertEqual((P / 'linkB' / 'linkD').readlink(), self.cls('../dirB'))
+        with self.assertRaises(OSError):
+            (P / 'fileA').readlink()
+
+    @unittest.skipIf(hasattr(os, "readlink"), "os.readlink() is present")
+    def test_readlink_unsupported(self):
+        P = self.cls(self.base)
+        p = P / 'fileA'
+        with self.assertRaises(pathlib.UnsupportedOperation):
+            q.readlink(p)
+
     @os_helper.skip_unless_working_chmod
     def test_chmod(self):
         p = self.cls(self.base) / 'fileA'
@@ -991,6 +1352,41 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
         self.assertEqual(expected_gid, gid_2)
         self.assertEqual(expected_name, link.group(follow_symlinks=False))
 
+    @needs_symlinks
+    def test_delete_symlink(self):
+        tmp = self.cls(self.base, 'delete')
+        tmp.mkdir()
+        dir_ = tmp / 'dir'
+        dir_.mkdir()
+        link = tmp / 'link'
+        link.symlink_to(dir_)
+        link._delete()
+        self.assertTrue(dir_.exists())
+        self.assertFalse(link.exists(follow_symlinks=False))
+
+    @needs_symlinks
+    def test_delete_inner_symlink(self):
+        tmp = self.cls(self.base, 'delete')
+        tmp.mkdir()
+        dir1 = tmp / 'dir1'
+        dir2 = dir1 / 'dir2'
+        dir3 = tmp / 'dir3'
+        for d in dir1, dir2, dir3:
+            d.mkdir()
+        file1 = tmp / 'file1'
+        file1.write_text('foo')
+        link1 = dir1 / 'link1'
+        link1.symlink_to(dir2)
+        link2 = dir1 / 'link2'
+        link2.symlink_to(dir3)
+        link3 = dir1 / 'link3'
+        link3.symlink_to(file1)
+        # make sure symlinks are removed but not followed
+        dir1._delete()
+        self.assertFalse(dir1.exists())
+        self.assertTrue(dir3.exists())
+        self.assertTrue(file1.exists())
+
     @unittest.skipIf(sys.platform[:6] == 'cygwin',
                      "This test can't be run on Cygwin (issue #1071513).")
     @os_helper.skip_if_dac_override
@@ -1354,6 +1750,12 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
         with self.assertRaises(pathlib.UnsupportedOperation):
             q.symlink_to(p)
 
+    @needs_symlinks
+    def test_stat_no_follow_symlinks(self):
+        p = self.cls(self.base) / 'linkA'
+        st = p.stat()
+        self.assertNotEqual(st, p.stat(follow_symlinks=False))
+
     @needs_symlinks
     def test_lstat(self):
         p = self.cls(self.base)/ 'linkA'
@@ -1433,6 +1835,15 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
         with self.assertRaises(TypeError):
             self.cls(foo="bar")
 
+    @needs_symlinks
+    def test_iterdir_symlink(self):
+        # __iter__ on a symlink to a directory.
+        P = self.cls
+        p = P(self.base, 'linkB')
+        paths = set(p.iterdir())
+        expected = { P(self.base, 'linkB', q) for q in ['fileB', 'linkD'] }
+        self.assertEqual(paths, expected)
+
     def test_glob_empty_pattern(self):
         p = self.cls('')
         with self.assertRaisesRegex(ValueError, 'Unacceptable pattern'):
@@ -1493,6 +1904,25 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
             self.assertEqual(
                 set(P('.').glob('**/*/*')), {P("dirD/fileD")})
 
+    # See https://github.com/WebAssembly/wasi-filesystem/issues/26
+    @unittest.skipIf(is_wasi, "WASI resolution of '..' parts doesn't match POSIX")
+    def test_glob_dotdot(self):
+        # ".." is not special in globs.
+        P = self.cls
+        p = P(self.base)
+        self.assertEqual(set(p.glob("..")), { P(self.base, "..") })
+        self.assertEqual(set(p.glob("../..")), { P(self.base, "..", "..") })
+        self.assertEqual(set(p.glob("dirA/..")), { P(self.base, "dirA", "..") })
+        self.assertEqual(set(p.glob("dirA/../file*")), { P(self.base, "dirA/../fileA") })
+        self.assertEqual(set(p.glob("dirA/../file*/..")), set())
+        self.assertEqual(set(p.glob("../xyzzy")), set())
+        if self.cls.parser is posixpath:
+            self.assertEqual(set(p.glob("xyzzy/..")), set())
+        else:
+            # ".." segments are normalized first on Windows, so this path is stat()able.
+            self.assertEqual(set(p.glob("xyzzy/..")), { P(self.base, "xyzzy", "..") })
+        self.assertEqual(set(p.glob("/".join([".."] * 50))), { P(self.base, *[".."] * 50)})
+
     def test_glob_inaccessible(self):
         P = self.cls
         p = P(self.base, "mydir1", "mydir2")
@@ -1508,6 +1938,124 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
         self.assertEqual(expect, set(p.rglob(P(pattern))))
         self.assertEqual(expect, set(p.rglob(FakePath(pattern))))
 
+    @needs_symlinks
+    @unittest.skipIf(is_emscripten, "Hangs")
+    def test_glob_recurse_symlinks_common(self):
+        def _check(path, glob, expected):
+            actual = {path for path in path.glob(glob, recurse_symlinks=True)
+                      if path.parts.count("linkD") <= 1}  # exclude symlink loop.
+            self.assertEqual(actual, { P(self.base, q) for q in expected })
+        P = self.cls
+        p = P(self.base)
+        _check(p, "fileB", [])
+        _check(p, "dir*/file*", ["dirB/fileB", "dirC/fileC"])
+        _check(p, "*A", ["dirA", "fileA", "linkA"])
+        _check(p, "*B/*", ["dirB/fileB", "dirB/linkD", "linkB/fileB", "linkB/linkD"])
+        _check(p, "*/fileB", ["dirB/fileB", "linkB/fileB"])
+        _check(p, "*/", ["dirA/", "dirB/", "dirC/", "dirE/", "linkB/"])
+        _check(p, "dir*/*/..", ["dirC/dirD/..", "dirA/linkC/..", "dirB/linkD/.."])
+        _check(p, "dir*/**", [
+            "dirA/", "dirA/linkC", "dirA/linkC/fileB", "dirA/linkC/linkD", "dirA/linkC/linkD/fileB",
+            "dirB/", "dirB/fileB", "dirB/linkD", "dirB/linkD/fileB",
+            "dirC/", "dirC/fileC", "dirC/dirD",  "dirC/dirD/fileD", "dirC/novel.txt",
+            "dirE/"])
+        _check(p, "dir*/**/", ["dirA/", "dirA/linkC/", "dirA/linkC/linkD/", "dirB/", "dirB/linkD/",
+                               "dirC/", "dirC/dirD/", "dirE/"])
+        _check(p, "dir*/**/..", ["dirA/..", "dirA/linkC/..", "dirB/..",
+                                 "dirB/linkD/..", "dirA/linkC/linkD/..",
+                                 "dirC/..", "dirC/dirD/..", "dirE/.."])
+        _check(p, "dir*/*/**", [
+            "dirA/linkC/", "dirA/linkC/linkD", "dirA/linkC/fileB", "dirA/linkC/linkD/fileB",
+            "dirB/linkD/", "dirB/linkD/fileB",
+            "dirC/dirD/", "dirC/dirD/fileD"])
+        _check(p, "dir*/*/**/", ["dirA/linkC/", "dirA/linkC/linkD/", "dirB/linkD/", "dirC/dirD/"])
+        _check(p, "dir*/*/**/..", ["dirA/linkC/..", "dirA/linkC/linkD/..",
+                                   "dirB/linkD/..", "dirC/dirD/.."])
+        _check(p, "dir*/**/fileC", ["dirC/fileC"])
+        _check(p, "dir*/*/../dirD/**/", ["dirC/dirD/../dirD/"])
+        _check(p, "*/dirD/**", ["dirC/dirD/", "dirC/dirD/fileD"])
+        _check(p, "*/dirD/**/", ["dirC/dirD/"])
+
+    @needs_symlinks
+    @unittest.skipIf(is_emscripten, "Hangs")
+    def test_rglob_recurse_symlinks_common(self):
+        def _check(path, glob, expected):
+            actual = {path for path in path.rglob(glob, recurse_symlinks=True)
+                      if path.parts.count("linkD") <= 1}  # exclude symlink loop.
+            self.assertEqual(actual, { P(self.base, q) for q in expected })
+        P = self.cls
+        p = P(self.base)
+        _check(p, "fileB", ["dirB/fileB", "dirA/linkC/fileB", "linkB/fileB",
+                            "dirA/linkC/linkD/fileB", "dirB/linkD/fileB", "linkB/linkD/fileB"])
+        _check(p, "*/fileA", [])
+        _check(p, "*/fileB", ["dirB/fileB", "dirA/linkC/fileB", "linkB/fileB",
+                              "dirA/linkC/linkD/fileB", "dirB/linkD/fileB", "linkB/linkD/fileB"])
+        _check(p, "file*", ["fileA", "dirA/linkC/fileB", "dirB/fileB",
+                            "dirA/linkC/linkD/fileB", "dirB/linkD/fileB", "linkB/linkD/fileB",
+                            "dirC/fileC", "dirC/dirD/fileD", "linkB/fileB"])
+        _check(p, "*/", ["dirA/", "dirA/linkC/", "dirA/linkC/linkD/", "dirB/", "dirB/linkD/",
+                         "dirC/", "dirC/dirD/", "dirE/", "linkB/", "linkB/linkD/"])
+        _check(p, "", ["", "dirA/", "dirA/linkC/", "dirA/linkC/linkD/", "dirB/", "dirB/linkD/",
+                       "dirC/", "dirE/", "dirC/dirD/", "linkB/", "linkB/linkD/"])
+
+        p = P(self.base, "dirC")
+        _check(p, "*", ["dirC/fileC", "dirC/novel.txt",
+                        "dirC/dirD", "dirC/dirD/fileD"])
+        _check(p, "file*", ["dirC/fileC", "dirC/dirD/fileD"])
+        _check(p, "*/*", ["dirC/dirD/fileD"])
+        _check(p, "*/", ["dirC/dirD/"])
+        _check(p, "", ["dirC/", "dirC/dirD/"])
+        # gh-91616, a re module regression
+        _check(p, "*.txt", ["dirC/novel.txt"])
+        _check(p, "*.*", ["dirC/novel.txt"])
+
+    @needs_symlinks
+    def test_rglob_symlink_loop(self):
+        # Don't get fooled by symlink loops (Issue #26012).
+        P = self.cls
+        p = P(self.base)
+        given = set(p.rglob('*', recurse_symlinks=False))
+        expect = {'brokenLink',
+                  'dirA', 'dirA/linkC',
+                  'dirB', 'dirB/fileB', 'dirB/linkD',
+                  'dirC', 'dirC/dirD', 'dirC/dirD/fileD',
+                  'dirC/fileC', 'dirC/novel.txt',
+                  'dirE',
+                  'fileA',
+                  'linkA',
+                  'linkB',
+                  'brokenLinkLoop',
+                  }
+        self.assertEqual(given, {p / x for x in expect})
+
+    @needs_symlinks
+    def test_glob_permissions(self):
+        # See bpo-38894
+        P = self.cls
+        base = P(self.base) / 'permissions'
+        base.mkdir()
+
+        for i in range(100):
+            link = base / f"link{i}"
+            if i % 2:
+                link.symlink_to(P(self.base, "dirE", "nonexistent"))
+            else:
+                link.symlink_to(P(self.base, "dirC"), target_is_directory=True)
+
+        self.assertEqual(len(set(base.glob("*"))), 100)
+        self.assertEqual(len(set(base.glob("*/"))), 50)
+        self.assertEqual(len(set(base.glob("*/fileC"))), 50)
+        self.assertEqual(len(set(base.glob("*/file*"))), 50)
+
+    @needs_symlinks
+    def test_glob_long_symlink(self):
+        # See gh-87695
+        base = self.cls(self.base) / 'long_symlink'
+        base.mkdir()
+        bad_link = base / 'bad_link'
+        bad_link.symlink_to("bad" * 200)
+        self.assertEqual(sorted(base.glob('**/*')), [bad_link])
+
     @needs_posix
     def test_absolute_posix(self):
         P = self.cls
@@ -1822,6 +2370,9 @@ class PathWalkTest(test_pathlib_abc.DummyPathWalkTest):
     can_symlink = PathTest.can_symlink
 
     def setUp(self):
+        name = self.id().split('.')[-1]
+        if name in _tests_needing_symlinks and not self.can_symlink:
+            self.skipTest('requires symlinks')
         super().setUp()
         sub21_path= self.sub2_path / "SUB21"
         tmp5_path = sub21_path / "tmp3"
@@ -1903,6 +2454,37 @@ class PathWalkTest(test_pathlib_abc.DummyPathWalkTest):
             list(base.walk())
             list(base.walk(top_down=False))
 
+    @needs_symlinks
+    def test_walk_follow_symlinks(self):
+        walk_it = self.walk_path.walk(follow_symlinks=True)
+        for root, dirs, files in walk_it:
+            if root == self.link_path:
+                self.assertEqual(dirs, [])
+                self.assertEqual(files, ["tmp4"])
+                break
+        else:
+            self.fail("Didn't follow symlink with follow_symlinks=True")
+
+    @needs_symlinks
+    def test_walk_symlink_location(self):
+        # Tests whether symlinks end up in filenames or dirnames depending
+        # on the `follow_symlinks` argument.
+        walk_it = self.walk_path.walk(follow_symlinks=False)
+        for root, dirs, files in walk_it:
+            if root == self.sub2_path:
+                self.assertIn("link", files)
+                break
+        else:
+            self.fail("symlink not found")
+
+        walk_it = self.walk_path.walk(follow_symlinks=True)
+        for root, dirs, files in walk_it:
+            if root == self.sub2_path:
+                self.assertIn("link", dirs)
+                break
+        else:
+            self.fail("symlink not found")
+
 
 @unittest.skipIf(os.name == 'nt', 'test requires a POSIX-compatible system')
 class PosixPathTest(PathTest, PurePosixPathTest):
index 00153e3f5e997e00b217b72a955c251f7b25ef88..bf9ae6cc8a2433e746d258bdfdce6787c4f4eda6 100644 (file)
@@ -8,13 +8,11 @@ import unittest
 from pathlib._abc import UnsupportedOperation, ParserBase, PurePathBase, PathBase
 import posixpath
 
-from test.support import is_wasi, is_emscripten
 from test.support.os_helper import TESTFN
 
 
 _tests_needing_posix = set()
 _tests_needing_windows = set()
-_tests_needing_symlinks = set()
 
 
 def needs_posix(fn):
@@ -27,11 +25,6 @@ def needs_windows(fn):
     _tests_needing_windows.add(fn.__name__)
     return fn
 
-def needs_symlinks(fn):
-    """Decorator that marks a test as requiring a path class that supports symlinks."""
-    _tests_needing_symlinks.add(fn.__name__)
-    return fn
-
 
 class UnsupportedOperationTest(unittest.TestCase):
     def test_is_notimplemented(self):
@@ -1369,7 +1362,6 @@ class PathBaseTest(PurePathBaseTest):
         self.assertRaises(e, p.glob, '*')
         self.assertRaises(e, p.rglob, '*')
         self.assertRaises(e, lambda: list(p.walk()))
-        self.assertRaises(e, p.absolute)
         self.assertRaises(e, p.expanduser)
         self.assertRaises(e, p.readlink)
         self.assertRaises(e, p.symlink_to, 'foo')
@@ -1425,7 +1417,6 @@ class DummyPath(PathBase):
 
     _files = {}
     _directories = {}
-    _symlinks = {}
 
     def __eq__(self, other):
         if not isinstance(other, DummyPath):
@@ -1439,16 +1430,11 @@ class DummyPath(PathBase):
         return "{}({!r})".format(self.__class__.__name__, self.as_posix())
 
     def stat(self, *, follow_symlinks=True):
-        if follow_symlinks or self.name in ('', '.', '..'):
-            path = str(self.resolve(strict=True))
-        else:
-            path = str(self.parent.resolve(strict=True) / self.name)
+        path = str(self).rstrip('/')
         if path in self._files:
             st_mode = stat.S_IFREG
         elif path in self._directories:
             st_mode = stat.S_IFDIR
-        elif path in self._symlinks:
-            st_mode = stat.S_IFLNK
         else:
             raise FileNotFoundError(errno.ENOENT, "Not found", str(self))
         return DummyPathStatResult(st_mode, hash(str(self)), 0, 0, 0, 0, 0, 0, 0, 0)
@@ -1457,10 +1443,7 @@ class DummyPath(PathBase):
              errors=None, newline=None):
         if buffering != -1 and not (buffering == 0 and 'b' in mode):
             raise NotImplementedError
-        path_obj = self.resolve()
-        path = str(path_obj)
-        name = path_obj.name
-        parent = str(path_obj.parent)
+        path = str(self)
         if path in self._directories:
             raise IsADirectoryError(errno.EISDIR, "Is a directory", path)
 
@@ -1471,6 +1454,7 @@ class DummyPath(PathBase):
                 raise FileNotFoundError(errno.ENOENT, "File not found", path)
             stream = io.BytesIO(self._files[path])
         elif mode == 'w':
+            parent, name = posixpath.split(path)
             if parent not in self._directories:
                 raise FileNotFoundError(errno.ENOENT, "File not found", parent)
             stream = DummyPathIO(self._files, path)
@@ -1483,7 +1467,7 @@ class DummyPath(PathBase):
         return stream
 
     def iterdir(self):
-        path = str(self.resolve())
+        path = str(self).rstrip('/')
         if path in self._files:
             raise NotADirectoryError(errno.ENOTDIR, "Not a directory", path)
         elif path in self._directories:
@@ -1492,9 +1476,9 @@ class DummyPath(PathBase):
             raise FileNotFoundError(errno.ENOENT, "File not found", path)
 
     def mkdir(self, mode=0o777, parents=False, exist_ok=False):
-        path = str(self.parent.resolve() / self.name)
-        parent = str(self.parent.resolve())
-        if path in self._directories or path in self._symlinks:
+        path = str(self)
+        parent = str(self.parent)
+        if path in self._directories:
             if exist_ok:
                 return
             else:
@@ -1510,33 +1494,28 @@ class DummyPath(PathBase):
             self.mkdir(mode, parents=False, exist_ok=exist_ok)
 
     def unlink(self, missing_ok=False):
-        path_obj = self.parent.resolve(strict=True) / self.name
-        path = str(path_obj)
-        name = path_obj.name
-        parent = str(path_obj.parent)
+        path = str(self)
+        name = self.name
+        parent = str(self.parent)
         if path in self._directories:
             raise IsADirectoryError(errno.EISDIR, "Is a directory", path)
         elif path in self._files:
             self._directories[parent].remove(name)
             del self._files[path]
-        elif path in self._symlinks:
-            self._directories[parent].remove(name)
-            del self._symlinks[path]
         elif not missing_ok:
             raise FileNotFoundError(errno.ENOENT, "File not found", path)
 
     def rmdir(self):
-        path_obj = self.parent.resolve(strict=True) / self.name
-        path = str(path_obj)
-        if path in self._files or path in self._symlinks:
+        path = str(self)
+        if path in self._files:
             raise NotADirectoryError(errno.ENOTDIR, "Not a directory", path)
         elif path not in self._directories:
             raise FileNotFoundError(errno.ENOENT, "File not found", path)
         elif self._directories[path]:
             raise OSError(errno.ENOTEMPTY, "Directory not empty", path)
         else:
-            name = path_obj.name
-            parent = str(path_obj.parent)
+            name = self.name
+            parent = str(self.parent)
             self._directories[parent].remove(name)
             del self._directories[path]
 
@@ -1569,9 +1548,6 @@ class DummyPathTest(DummyPurePathTest):
 
     def setUp(self):
         super().setUp()
-        name = self.id().split('.')[-1]
-        if name in _tests_needing_symlinks and not self.can_symlink:
-            self.skipTest('requires symlinks')
         parser = self.cls.parser
         p = self.cls(self.base)
         p.mkdir(parents=True)
@@ -1604,7 +1580,6 @@ class DummyPathTest(DummyPurePathTest):
         cls = self.cls
         cls._files.clear()
         cls._directories.clear()
-        cls._symlinks.clear()
 
     def tempdir(self):
         path = self.cls(self.base).with_name('tmp-dirD')
@@ -1730,101 +1705,6 @@ class DummyPathTest(DummyPurePathTest):
         self.assertTrue(target.exists())
         self.assertEqual(source.read_text(), target.read_text())
 
-    @needs_symlinks
-    def test_copy_symlink_follow_symlinks_true(self):
-        base = self.cls(self.base)
-        source = base / 'linkA'
-        target = base / 'copyA'
-        result = source.copy(target)
-        self.assertEqual(result, target)
-        self.assertTrue(target.exists())
-        self.assertFalse(target.is_symlink())
-        self.assertEqual(source.read_text(), target.read_text())
-
-    @needs_symlinks
-    def test_copy_symlink_follow_symlinks_false(self):
-        base = self.cls(self.base)
-        source = base / 'linkA'
-        target = base / 'copyA'
-        result = source.copy(target, follow_symlinks=False)
-        self.assertEqual(result, target)
-        self.assertTrue(target.exists())
-        self.assertTrue(target.is_symlink())
-        self.assertEqual(source.readlink(), target.readlink())
-
-    @needs_symlinks
-    def test_copy_symlink_to_itself(self):
-        base = self.cls(self.base)
-        source = base / 'linkA'
-        self.assertRaises(OSError, source.copy, source)
-
-    @needs_symlinks
-    def test_copy_symlink_to_existing_symlink(self):
-        base = self.cls(self.base)
-        source = base / 'copySource'
-        target = base / 'copyTarget'
-        source.symlink_to(base / 'fileA')
-        target.symlink_to(base / 'dirC')
-        self.assertRaises(OSError, source.copy, target)
-        self.assertRaises(OSError, source.copy, target, follow_symlinks=False)
-
-    @needs_symlinks
-    def test_copy_symlink_to_existing_directory_symlink(self):
-        base = self.cls(self.base)
-        source = base / 'copySource'
-        target = base / 'copyTarget'
-        source.symlink_to(base / 'fileA')
-        target.symlink_to(base / 'dirC')
-        self.assertRaises(OSError, source.copy, target)
-        self.assertRaises(OSError, source.copy, target, follow_symlinks=False)
-
-    @needs_symlinks
-    def test_copy_directory_symlink_follow_symlinks_false(self):
-        base = self.cls(self.base)
-        source = base / 'linkB'
-        target = base / 'copyA'
-        result = source.copy(target, follow_symlinks=False)
-        self.assertEqual(result, target)
-        self.assertTrue(target.exists())
-        self.assertTrue(target.is_symlink())
-        self.assertEqual(source.readlink(), target.readlink())
-
-    @needs_symlinks
-    def test_copy_directory_symlink_to_itself(self):
-        base = self.cls(self.base)
-        source = base / 'linkB'
-        self.assertRaises(OSError, source.copy, source)
-        self.assertRaises(OSError, source.copy, source, follow_symlinks=False)
-
-    @needs_symlinks
-    def test_copy_directory_symlink_into_itself(self):
-        base = self.cls(self.base)
-        source = base / 'linkB'
-        target = base / 'linkB' / 'copyB'
-        self.assertRaises(OSError, source.copy, target)
-        self.assertRaises(OSError, source.copy, target, follow_symlinks=False)
-        self.assertFalse(target.exists())
-
-    @needs_symlinks
-    def test_copy_directory_symlink_to_existing_symlink(self):
-        base = self.cls(self.base)
-        source = base / 'copySource'
-        target = base / 'copyTarget'
-        source.symlink_to(base / 'dirC')
-        target.symlink_to(base / 'fileA')
-        self.assertRaises(FileExistsError, source.copy, target)
-        self.assertRaises(FileExistsError, source.copy, target, follow_symlinks=False)
-
-    @needs_symlinks
-    def test_copy_directory_symlink_to_existing_directory_symlink(self):
-        base = self.cls(self.base)
-        source = base / 'copySource'
-        target = base / 'copyTarget'
-        source.symlink_to(base / 'dirC' / 'dirD')
-        target.symlink_to(base / 'dirC')
-        self.assertRaises(FileExistsError, source.copy, target)
-        self.assertRaises(FileExistsError, source.copy, target, follow_symlinks=False)
-
     def test_copy_file_to_existing_file(self):
         base = self.cls(self.base)
         source = base / 'fileA'
@@ -1840,34 +1720,6 @@ class DummyPathTest(DummyPurePathTest):
         target = base / 'dirA'
         self.assertRaises(OSError, source.copy, target)
 
-    @needs_symlinks
-    def test_copy_file_to_existing_symlink(self):
-        base = self.cls(self.base)
-        source = base / 'dirB' / 'fileB'
-        target = base / 'linkA'
-        real_target = base / 'fileA'
-        result = source.copy(target)
-        self.assertEqual(result, target)
-        self.assertTrue(target.exists())
-        self.assertTrue(target.is_symlink())
-        self.assertTrue(real_target.exists())
-        self.assertFalse(real_target.is_symlink())
-        self.assertEqual(source.read_text(), real_target.read_text())
-
-    @needs_symlinks
-    def test_copy_file_to_existing_symlink_follow_symlinks_false(self):
-        base = self.cls(self.base)
-        source = base / 'dirB' / 'fileB'
-        target = base / 'linkA'
-        real_target = base / 'fileA'
-        result = source.copy(target, follow_symlinks=False)
-        self.assertEqual(result, target)
-        self.assertTrue(target.exists())
-        self.assertTrue(target.is_symlink())
-        self.assertTrue(real_target.exists())
-        self.assertFalse(real_target.is_symlink())
-        self.assertEqual(source.read_text(), real_target.read_text())
-
     def test_copy_file_empty(self):
         base = self.cls(self.base)
         source = base / 'empty'
@@ -1985,23 +1837,6 @@ class DummyPathTest(DummyPurePathTest):
         self.assertRaises(OSError, source.copy, target, follow_symlinks=False)
         self.assertFalse(target.exists())
 
-    @needs_symlinks
-    def test_copy_dangling_symlink(self):
-        base = self.cls(self.base)
-        source = base / 'source'
-        target = base / 'target'
-
-        source.mkdir()
-        source.joinpath('link').symlink_to('nonexistent')
-
-        self.assertRaises(FileNotFoundError, source.copy, target)
-
-        target2 = base / 'target2'
-        result = source.copy(target2, follow_symlinks=False)
-        self.assertEqual(result, target2)
-        self.assertTrue(target2.joinpath('link').is_symlink())
-        self.assertEqual(target2.joinpath('link').readlink(), self.cls('nonexistent'))
-
     def test_copy_into(self):
         base = self.cls(self.base)
         source = base / 'fileA'
@@ -2087,54 +1922,6 @@ class DummyPathTest(DummyPurePathTest):
         self.assertTrue(source.exists())
         self.assertFalse(target.exists())
 
-    @needs_symlinks
-    def test_move_file_symlink(self):
-        base = self.cls(self.base)
-        source = base / 'linkA'
-        source_readlink = source.readlink()
-        target = base / 'linkA_moved'
-        result = source.move(target)
-        self.assertEqual(result, target)
-        self.assertFalse(source.exists())
-        self.assertTrue(target.is_symlink())
-        self.assertEqual(source_readlink, target.readlink())
-
-    @needs_symlinks
-    def test_move_file_symlink_to_itself(self):
-        base = self.cls(self.base)
-        source = base / 'linkA'
-        self.assertRaises(OSError, source.move, source)
-
-    @needs_symlinks
-    def test_move_dir_symlink(self):
-        base = self.cls(self.base)
-        source = base / 'linkB'
-        source_readlink = source.readlink()
-        target = base / 'linkB_moved'
-        result = source.move(target)
-        self.assertEqual(result, target)
-        self.assertFalse(source.exists())
-        self.assertTrue(target.is_symlink())
-        self.assertEqual(source_readlink, target.readlink())
-
-    @needs_symlinks
-    def test_move_dir_symlink_to_itself(self):
-        base = self.cls(self.base)
-        source = base / 'linkB'
-        self.assertRaises(OSError, source.move, source)
-
-    @needs_symlinks
-    def test_move_dangling_symlink(self):
-        base = self.cls(self.base)
-        source = base / 'brokenLink'
-        source_readlink = source.readlink()
-        target = base / 'brokenLink_moved'
-        result = source.move(target)
-        self.assertEqual(result, target)
-        self.assertFalse(source.exists())
-        self.assertTrue(target.is_symlink())
-        self.assertEqual(source_readlink, target.readlink())
-
     def test_move_into(self):
         base = self.cls(self.base)
         source = base / 'fileA'
@@ -2161,15 +1948,6 @@ class DummyPathTest(DummyPurePathTest):
             expected += ['linkA', 'linkB', 'brokenLink', 'brokenLinkLoop']
         self.assertEqual(paths, { P(self.base, q) for q in expected })
 
-    @needs_symlinks
-    def test_iterdir_symlink(self):
-        # __iter__ on a symlink to a directory.
-        P = self.cls
-        p = P(self.base, 'linkB')
-        paths = set(p.iterdir())
-        expected = { P(self.base, 'linkB', q) for q in ['fileB', 'linkD'] }
-        self.assertEqual(paths, expected)
-
     def test_iterdir_nodir(self):
         # __iter__ on something that is not a directory.
         p = self.cls(self.base, 'fileA')
@@ -2196,7 +1974,6 @@ class DummyPathTest(DummyPurePathTest):
                 if entry.name != 'brokenLinkLoop':
                     self.assertEqual(entry.is_dir(), child.is_dir())
 
-
     def test_glob_common(self):
         def _check(glob, expected):
             self.assertEqual(set(glob), { P(self.base, q) for q in expected })
@@ -2250,8 +2027,6 @@ class DummyPathTest(DummyPurePathTest):
         P = self.cls
         p = P(self.base)
         self.assertEqual(list(p.glob("")), [p])
-        self.assertEqual(list(p.glob(".")), [p / "."])
-        self.assertEqual(list(p.glob("./")), [p / "./"])
 
     def test_glob_case_sensitive(self):
         P = self.cls
@@ -2265,44 +2040,6 @@ class DummyPathTest(DummyPurePathTest):
         _check(path, "dirb/file*", True, [])
         _check(path, "dirb/file*", False, ["dirB/fileB"])
 
-    @needs_symlinks
-    @unittest.skipIf(is_emscripten, "Hangs")
-    def test_glob_recurse_symlinks_common(self):
-        def _check(path, glob, expected):
-            actual = {path for path in path.glob(glob, recurse_symlinks=True)
-                      if path.parts.count("linkD") <= 1}  # exclude symlink loop.
-            self.assertEqual(actual, { P(self.base, q) for q in expected })
-        P = self.cls
-        p = P(self.base)
-        _check(p, "fileB", [])
-        _check(p, "dir*/file*", ["dirB/fileB", "dirC/fileC"])
-        _check(p, "*A", ["dirA", "fileA", "linkA"])
-        _check(p, "*B/*", ["dirB/fileB", "dirB/linkD", "linkB/fileB", "linkB/linkD"])
-        _check(p, "*/fileB", ["dirB/fileB", "linkB/fileB"])
-        _check(p, "*/", ["dirA/", "dirB/", "dirC/", "dirE/", "linkB/"])
-        _check(p, "dir*/*/..", ["dirC/dirD/..", "dirA/linkC/..", "dirB/linkD/.."])
-        _check(p, "dir*/**", [
-            "dirA/", "dirA/linkC", "dirA/linkC/fileB", "dirA/linkC/linkD", "dirA/linkC/linkD/fileB",
-            "dirB/", "dirB/fileB", "dirB/linkD", "dirB/linkD/fileB",
-            "dirC/", "dirC/fileC", "dirC/dirD",  "dirC/dirD/fileD", "dirC/novel.txt",
-            "dirE/"])
-        _check(p, "dir*/**/", ["dirA/", "dirA/linkC/", "dirA/linkC/linkD/", "dirB/", "dirB/linkD/",
-                               "dirC/", "dirC/dirD/", "dirE/"])
-        _check(p, "dir*/**/..", ["dirA/..", "dirA/linkC/..", "dirB/..",
-                                 "dirB/linkD/..", "dirA/linkC/linkD/..",
-                                 "dirC/..", "dirC/dirD/..", "dirE/.."])
-        _check(p, "dir*/*/**", [
-            "dirA/linkC/", "dirA/linkC/linkD", "dirA/linkC/fileB", "dirA/linkC/linkD/fileB",
-            "dirB/linkD/", "dirB/linkD/fileB",
-            "dirC/dirD/", "dirC/dirD/fileD"])
-        _check(p, "dir*/*/**/", ["dirA/linkC/", "dirA/linkC/linkD/", "dirB/linkD/", "dirC/dirD/"])
-        _check(p, "dir*/*/**/..", ["dirA/linkC/..", "dirA/linkC/linkD/..",
-                                   "dirB/linkD/..", "dirC/dirD/.."])
-        _check(p, "dir*/**/fileC", ["dirC/fileC"])
-        _check(p, "dir*/*/../dirD/**/", ["dirC/dirD/../dirD/"])
-        _check(p, "*/dirD/**", ["dirC/dirD/", "dirC/dirD/fileD"])
-        _check(p, "*/dirD/**/", ["dirC/dirD/"])
-
     def test_rglob_recurse_symlinks_false(self):
         def _check(path, glob, expected):
             actual = set(path.rglob(glob, recurse_symlinks=False))
@@ -2361,252 +2098,6 @@ class DummyPathTest(DummyPurePathTest):
         self.assertEqual(set(p.rglob("FILEd")), { P(self.base, "dirC/dirD/fileD") })
         self.assertEqual(set(p.rglob("*\\")), { P(self.base, "dirC/dirD/") })
 
-    @needs_symlinks
-    @unittest.skipIf(is_emscripten, "Hangs")
-    def test_rglob_recurse_symlinks_common(self):
-        def _check(path, glob, expected):
-            actual = {path for path in path.rglob(glob, recurse_symlinks=True)
-                      if path.parts.count("linkD") <= 1}  # exclude symlink loop.
-            self.assertEqual(actual, { P(self.base, q) for q in expected })
-        P = self.cls
-        p = P(self.base)
-        _check(p, "fileB", ["dirB/fileB", "dirA/linkC/fileB", "linkB/fileB",
-                            "dirA/linkC/linkD/fileB", "dirB/linkD/fileB", "linkB/linkD/fileB"])
-        _check(p, "*/fileA", [])
-        _check(p, "*/fileB", ["dirB/fileB", "dirA/linkC/fileB", "linkB/fileB",
-                              "dirA/linkC/linkD/fileB", "dirB/linkD/fileB", "linkB/linkD/fileB"])
-        _check(p, "file*", ["fileA", "dirA/linkC/fileB", "dirB/fileB",
-                            "dirA/linkC/linkD/fileB", "dirB/linkD/fileB", "linkB/linkD/fileB",
-                            "dirC/fileC", "dirC/dirD/fileD", "linkB/fileB"])
-        _check(p, "*/", ["dirA/", "dirA/linkC/", "dirA/linkC/linkD/", "dirB/", "dirB/linkD/",
-                         "dirC/", "dirC/dirD/", "dirE/", "linkB/", "linkB/linkD/"])
-        _check(p, "", ["", "dirA/", "dirA/linkC/", "dirA/linkC/linkD/", "dirB/", "dirB/linkD/",
-                       "dirC/", "dirE/", "dirC/dirD/", "linkB/", "linkB/linkD/"])
-
-        p = P(self.base, "dirC")
-        _check(p, "*", ["dirC/fileC", "dirC/novel.txt",
-                        "dirC/dirD", "dirC/dirD/fileD"])
-        _check(p, "file*", ["dirC/fileC", "dirC/dirD/fileD"])
-        _check(p, "*/*", ["dirC/dirD/fileD"])
-        _check(p, "*/", ["dirC/dirD/"])
-        _check(p, "", ["dirC/", "dirC/dirD/"])
-        # gh-91616, a re module regression
-        _check(p, "*.txt", ["dirC/novel.txt"])
-        _check(p, "*.*", ["dirC/novel.txt"])
-
-    @needs_symlinks
-    def test_rglob_symlink_loop(self):
-        # Don't get fooled by symlink loops (Issue #26012).
-        P = self.cls
-        p = P(self.base)
-        given = set(p.rglob('*', recurse_symlinks=False))
-        expect = {'brokenLink',
-                  'dirA', 'dirA/linkC',
-                  'dirB', 'dirB/fileB', 'dirB/linkD',
-                  'dirC', 'dirC/dirD', 'dirC/dirD/fileD',
-                  'dirC/fileC', 'dirC/novel.txt',
-                  'dirE',
-                  'fileA',
-                  'linkA',
-                  'linkB',
-                  'brokenLinkLoop',
-                  }
-        self.assertEqual(given, {p / x for x in expect})
-
-    # See https://github.com/WebAssembly/wasi-filesystem/issues/26
-    @unittest.skipIf(is_wasi, "WASI resolution of '..' parts doesn't match POSIX")
-    def test_glob_dotdot(self):
-        # ".." is not special in globs.
-        P = self.cls
-        p = P(self.base)
-        self.assertEqual(set(p.glob("..")), { P(self.base, "..") })
-        self.assertEqual(set(p.glob("../..")), { P(self.base, "..", "..") })
-        self.assertEqual(set(p.glob("dirA/..")), { P(self.base, "dirA", "..") })
-        self.assertEqual(set(p.glob("dirA/../file*")), { P(self.base, "dirA/../fileA") })
-        self.assertEqual(set(p.glob("dirA/../file*/..")), set())
-        self.assertEqual(set(p.glob("../xyzzy")), set())
-        if self.cls.parser is posixpath:
-            self.assertEqual(set(p.glob("xyzzy/..")), set())
-        else:
-            # ".." segments are normalized first on Windows, so this path is stat()able.
-            self.assertEqual(set(p.glob("xyzzy/..")), { P(self.base, "xyzzy", "..") })
-        self.assertEqual(set(p.glob("/".join([".."] * 50))), { P(self.base, *[".."] * 50)})
-
-    @needs_symlinks
-    def test_glob_permissions(self):
-        # See bpo-38894
-        P = self.cls
-        base = P(self.base) / 'permissions'
-        base.mkdir()
-
-        for i in range(100):
-            link = base / f"link{i}"
-            if i % 2:
-                link.symlink_to(P(self.base, "dirE", "nonexistent"))
-            else:
-                link.symlink_to(P(self.base, "dirC"), target_is_directory=True)
-
-        self.assertEqual(len(set(base.glob("*"))), 100)
-        self.assertEqual(len(set(base.glob("*/"))), 50)
-        self.assertEqual(len(set(base.glob("*/fileC"))), 50)
-        self.assertEqual(len(set(base.glob("*/file*"))), 50)
-
-    @needs_symlinks
-    def test_glob_long_symlink(self):
-        # See gh-87695
-        base = self.cls(self.base) / 'long_symlink'
-        base.mkdir()
-        bad_link = base / 'bad_link'
-        bad_link.symlink_to("bad" * 200)
-        self.assertEqual(sorted(base.glob('**/*')), [bad_link])
-
-    @needs_posix
-    def test_absolute_posix(self):
-        P = self.cls
-        # The default implementation uses '/' as the current directory
-        self.assertEqual(str(P('').absolute()), '/')
-        self.assertEqual(str(P('a').absolute()), '/a')
-        self.assertEqual(str(P('a/b').absolute()), '/a/b')
-
-        self.assertEqual(str(P('/').absolute()), '/')
-        self.assertEqual(str(P('/a').absolute()), '/a')
-        self.assertEqual(str(P('/a/b').absolute()), '/a/b')
-
-        # '//'-prefixed absolute path (supported by POSIX).
-        self.assertEqual(str(P('//').absolute()), '//')
-        self.assertEqual(str(P('//a').absolute()), '//a')
-        self.assertEqual(str(P('//a/b').absolute()), '//a/b')
-
-    @needs_symlinks
-    def test_readlink(self):
-        P = self.cls(self.base)
-        self.assertEqual((P / 'linkA').readlink(), self.cls('fileA'))
-        self.assertEqual((P / 'brokenLink').readlink(),
-                         self.cls('non-existing'))
-        self.assertEqual((P / 'linkB').readlink(), self.cls('dirB'))
-        self.assertEqual((P / 'linkB' / 'linkD').readlink(), self.cls('../dirB'))
-        with self.assertRaises(OSError):
-            (P / 'fileA').readlink()
-
-    @unittest.skipIf(hasattr(os, "readlink"), "os.readlink() is present")
-    def test_readlink_unsupported(self):
-        P = self.cls(self.base)
-        p = P / 'fileA'
-        with self.assertRaises(UnsupportedOperation):
-            q.readlink(p)
-
-    def _check_resolve(self, p, expected, strict=True):
-        q = p.resolve(strict)
-        self.assertEqual(q, expected)
-
-    # This can be used to check both relative and absolute resolutions.
-    _check_resolve_relative = _check_resolve_absolute = _check_resolve
-
-    @needs_symlinks
-    def test_resolve_common(self):
-        P = self.cls
-        p = P(self.base, 'foo')
-        with self.assertRaises(OSError) as cm:
-            p.resolve(strict=True)
-        self.assertEqual(cm.exception.errno, errno.ENOENT)
-        # Non-strict
-        parser = self.parser
-        self.assertEqualNormCase(str(p.resolve(strict=False)),
-                                 parser.join(self.base, 'foo'))
-        p = P(self.base, 'foo', 'in', 'spam')
-        self.assertEqualNormCase(str(p.resolve(strict=False)),
-                                 parser.join(self.base, 'foo', 'in', 'spam'))
-        p = P(self.base, '..', 'foo', 'in', 'spam')
-        self.assertEqualNormCase(str(p.resolve(strict=False)),
-                                 parser.join(parser.dirname(self.base), 'foo', 'in', 'spam'))
-        # These are all relative symlinks.
-        p = P(self.base, 'dirB', 'fileB')
-        self._check_resolve_relative(p, p)
-        p = P(self.base, 'linkA')
-        self._check_resolve_relative(p, P(self.base, 'fileA'))
-        p = P(self.base, 'dirA', 'linkC', 'fileB')
-        self._check_resolve_relative(p, P(self.base, 'dirB', 'fileB'))
-        p = P(self.base, 'dirB', 'linkD', 'fileB')
-        self._check_resolve_relative(p, P(self.base, 'dirB', 'fileB'))
-        # Non-strict
-        p = P(self.base, 'dirA', 'linkC', 'fileB', 'foo', 'in', 'spam')
-        self._check_resolve_relative(p, P(self.base, 'dirB', 'fileB', 'foo', 'in',
-                                          'spam'), False)
-        p = P(self.base, 'dirA', 'linkC', '..', 'foo', 'in', 'spam')
-        if self.cls.parser is not posixpath:
-            # In Windows, if linkY points to dirB, 'dirA\linkY\..'
-            # resolves to 'dirA' without resolving linkY first.
-            self._check_resolve_relative(p, P(self.base, 'dirA', 'foo', 'in',
-                                              'spam'), False)
-        else:
-            # In Posix, if linkY points to dirB, 'dirA/linkY/..'
-            # resolves to 'dirB/..' first before resolving to parent of dirB.
-            self._check_resolve_relative(p, P(self.base, 'foo', 'in', 'spam'), False)
-        # Now create absolute symlinks.
-        d = self.tempdir()
-        P(self.base, 'dirA', 'linkX').symlink_to(d)
-        P(self.base, str(d), 'linkY').symlink_to(self.parser.join(self.base, 'dirB'))
-        p = P(self.base, 'dirA', 'linkX', 'linkY', 'fileB')
-        self._check_resolve_absolute(p, P(self.base, 'dirB', 'fileB'))
-        # Non-strict
-        p = P(self.base, 'dirA', 'linkX', 'linkY', 'foo', 'in', 'spam')
-        self._check_resolve_relative(p, P(self.base, 'dirB', 'foo', 'in', 'spam'),
-                                     False)
-        p = P(self.base, 'dirA', 'linkX', 'linkY', '..', 'foo', 'in', 'spam')
-        if self.cls.parser is not posixpath:
-            # In Windows, if linkY points to dirB, 'dirA\linkY\..'
-            # resolves to 'dirA' without resolving linkY first.
-            self._check_resolve_relative(p, P(d, 'foo', 'in', 'spam'), False)
-        else:
-            # In Posix, if linkY points to dirB, 'dirA/linkY/..'
-            # resolves to 'dirB/..' first before resolving to parent of dirB.
-            self._check_resolve_relative(p, P(self.base, 'foo', 'in', 'spam'), False)
-
-    @needs_symlinks
-    def test_resolve_dot(self):
-        # See http://web.archive.org/web/20200623062557/https://bitbucket.org/pitrou/pathlib/issues/9/
-        parser = self.parser
-        p = self.cls(self.base)
-        p.joinpath('0').symlink_to('.', target_is_directory=True)
-        p.joinpath('1').symlink_to(parser.join('0', '0'), target_is_directory=True)
-        p.joinpath('2').symlink_to(parser.join('1', '1'), target_is_directory=True)
-        q = p / '2'
-        self.assertEqual(q.resolve(strict=True), p)
-        r = q / '3' / '4'
-        self.assertRaises(FileNotFoundError, r.resolve, strict=True)
-        # Non-strict
-        self.assertEqual(r.resolve(strict=False), p / '3' / '4')
-
-    def _check_symlink_loop(self, *args):
-        path = self.cls(*args)
-        with self.assertRaises(OSError) as cm:
-            path.resolve(strict=True)
-        self.assertEqual(cm.exception.errno, errno.ELOOP)
-
-    @needs_posix
-    @needs_symlinks
-    def test_resolve_loop(self):
-        # Loops with relative symlinks.
-        self.cls(self.base, 'linkX').symlink_to('linkX/inside')
-        self._check_symlink_loop(self.base, 'linkX')
-        self.cls(self.base, 'linkY').symlink_to('linkY')
-        self._check_symlink_loop(self.base, 'linkY')
-        self.cls(self.base, 'linkZ').symlink_to('linkZ/../linkZ')
-        self._check_symlink_loop(self.base, 'linkZ')
-        # Non-strict
-        p = self.cls(self.base, 'linkZ', 'foo')
-        self.assertEqual(p.resolve(strict=False), p)
-        # Loops with absolute symlinks.
-        self.cls(self.base, 'linkU').symlink_to(self.parser.join(self.base, 'linkU/inside'))
-        self._check_symlink_loop(self.base, 'linkU')
-        self.cls(self.base, 'linkV').symlink_to(self.parser.join(self.base, 'linkV'))
-        self._check_symlink_loop(self.base, 'linkV')
-        self.cls(self.base, 'linkW').symlink_to(self.parser.join(self.base, 'linkW/../linkW'))
-        self._check_symlink_loop(self.base, 'linkW')
-        # Non-strict
-        q = self.cls(self.base, 'linkW', 'foo')
-        self.assertEqual(q.resolve(strict=False), q)
-
     def test_stat(self):
         statA = self.cls(self.base).joinpath('fileA').stat()
         statB = self.cls(self.base).joinpath('dirB', 'fileB').stat()
@@ -2627,12 +2118,6 @@ class DummyPathTest(DummyPurePathTest):
         self.assertEqual(statA.st_dev, statC.st_dev)
         # other attributes not used by pathlib.
 
-    @needs_symlinks
-    def test_stat_no_follow_symlinks(self):
-        p = self.cls(self.base) / 'linkA'
-        st = p.stat()
-        self.assertNotEqual(st, p.stat(follow_symlinks=False))
-
     def test_stat_no_follow_symlinks_nosymlink(self):
         p = self.cls(self.base) / 'fileA'
         st = p.stat()
@@ -2760,41 +2245,6 @@ class DummyPathTest(DummyPurePathTest):
         self.assertIs((P / 'fileA\udfff').is_char_device(), False)
         self.assertIs((P / 'fileA\x00').is_char_device(), False)
 
-    def _check_complex_symlinks(self, link0_target):
-        # Test solving a non-looping chain of symlinks (issue #19887).
-        parser = self.parser
-        P = self.cls(self.base)
-        P.joinpath('link1').symlink_to(parser.join('link0', 'link0'), target_is_directory=True)
-        P.joinpath('link2').symlink_to(parser.join('link1', 'link1'), target_is_directory=True)
-        P.joinpath('link3').symlink_to(parser.join('link2', 'link2'), target_is_directory=True)
-        P.joinpath('link0').symlink_to(link0_target, target_is_directory=True)
-
-        # Resolve absolute paths.
-        p = (P / 'link0').resolve()
-        self.assertEqual(p, P)
-        self.assertEqualNormCase(str(p), self.base)
-        p = (P / 'link1').resolve()
-        self.assertEqual(p, P)
-        self.assertEqualNormCase(str(p), self.base)
-        p = (P / 'link2').resolve()
-        self.assertEqual(p, P)
-        self.assertEqualNormCase(str(p), self.base)
-        p = (P / 'link3').resolve()
-        self.assertEqual(p, P)
-        self.assertEqualNormCase(str(p), self.base)
-
-    @needs_symlinks
-    def test_complex_symlinks_absolute(self):
-        self._check_complex_symlinks(self.base)
-
-    @needs_symlinks
-    def test_complex_symlinks_relative(self):
-        self._check_complex_symlinks('.')
-
-    @needs_symlinks
-    def test_complex_symlinks_relative_dot_dot(self):
-        self._check_complex_symlinks(self.parser.join('dirA', '..'))
-
     def test_unlink(self):
         p = self.cls(self.base) / 'fileA'
         p.unlink()
@@ -2838,41 +2288,6 @@ class DummyPathTest(DummyPurePathTest):
         self.assertRaises(FileNotFoundError, base.joinpath('dirC', 'fileC').stat)
         self.assertRaises(FileNotFoundError, base.joinpath('dirC', 'novel.txt').stat)
 
-    @needs_symlinks
-    def test_delete_symlink(self):
-        tmp = self.cls(self.base, 'delete')
-        tmp.mkdir()
-        dir_ = tmp / 'dir'
-        dir_.mkdir()
-        link = tmp / 'link'
-        link.symlink_to(dir_)
-        link._delete()
-        self.assertTrue(dir_.exists())
-        self.assertFalse(link.exists(follow_symlinks=False))
-
-    @needs_symlinks
-    def test_delete_inner_symlink(self):
-        tmp = self.cls(self.base, 'delete')
-        tmp.mkdir()
-        dir1 = tmp / 'dir1'
-        dir2 = dir1 / 'dir2'
-        dir3 = tmp / 'dir3'
-        for d in dir1, dir2, dir3:
-            d.mkdir()
-        file1 = tmp / 'file1'
-        file1.write_text('foo')
-        link1 = dir1 / 'link1'
-        link1.symlink_to(dir2)
-        link2 = dir1 / 'link2'
-        link2.symlink_to(dir3)
-        link3 = dir1 / 'link3'
-        link3.symlink_to(file1)
-        # make sure symlinks are removed but not followed
-        dir1._delete()
-        self.assertFalse(dir1.exists())
-        self.assertTrue(dir3.exists())
-        self.assertTrue(file1.exists())
-
     def test_delete_missing(self):
         tmp = self.cls(self.base, 'delete')
         tmp.mkdir()
@@ -2887,9 +2302,6 @@ class DummyPathWalkTest(unittest.TestCase):
     can_symlink = False
 
     def setUp(self):
-        name = self.id().split('.')[-1]
-        if name in _tests_needing_symlinks and not self.can_symlink:
-            self.skipTest('requires symlinks')
         # Build:
         #     TESTFN/
         #       TEST1/              a file kid and two directory kids
@@ -3002,70 +2414,6 @@ class DummyPathWalkTest(unittest.TestCase):
                 raise AssertionError(f"Unexpected path: {path}")
         self.assertTrue(seen_testfn)
 
-    @needs_symlinks
-    def test_walk_follow_symlinks(self):
-        walk_it = self.walk_path.walk(follow_symlinks=True)
-        for root, dirs, files in walk_it:
-            if root == self.link_path:
-                self.assertEqual(dirs, [])
-                self.assertEqual(files, ["tmp4"])
-                break
-        else:
-            self.fail("Didn't follow symlink with follow_symlinks=True")
-
-    @needs_symlinks
-    def test_walk_symlink_location(self):
-        # Tests whether symlinks end up in filenames or dirnames depending
-        # on the `follow_symlinks` argument.
-        walk_it = self.walk_path.walk(follow_symlinks=False)
-        for root, dirs, files in walk_it:
-            if root == self.sub2_path:
-                self.assertIn("link", files)
-                break
-        else:
-            self.fail("symlink not found")
-
-        walk_it = self.walk_path.walk(follow_symlinks=True)
-        for root, dirs, files in walk_it:
-            if root == self.sub2_path:
-                self.assertIn("link", dirs)
-                break
-        else:
-            self.fail("symlink not found")
-
-
-class DummyPathWithSymlinks(DummyPath):
-    __slots__ = ()
-
-    # Reduce symlink traversal limit to make tests run faster.
-    _max_symlinks = 20
-
-    def readlink(self):
-        path = str(self.parent.resolve() / self.name)
-        if path in self._symlinks:
-            return self.with_segments(self._symlinks[path][0])
-        elif path in self._files or path in self._directories:
-            raise OSError(errno.EINVAL, "Not a symlink", path)
-        else:
-            raise FileNotFoundError(errno.ENOENT, "File not found", path)
-
-    def symlink_to(self, target, target_is_directory=False):
-        path = str(self.parent.resolve() / self.name)
-        parent = str(self.parent.resolve())
-        if path in self._symlinks:
-            raise FileExistsError(errno.EEXIST, "File exists", path)
-        self._directories[parent].add(self.name)
-        self._symlinks[path] = str(target), target_is_directory
-
-
-class DummyPathWithSymlinksTest(DummyPathTest):
-    cls = DummyPathWithSymlinks
-    can_symlink = True
-
-
-class DummyPathWithSymlinksWalkTest(DummyPathWalkTest):
-    cls = DummyPathWithSymlinks
-    can_symlink = True
 
 
 if __name__ == "__main__":