[Python-checkins] gh-99547: Add isjunction methods for checking if a path is a junction (GH-99548)

zooba webhook-mailer at python.org
Tue Nov 22 12:19:41 EST 2022


https://github.com/python/cpython/commit/1b2de89bce7eee3c63ce2286f071db57cd2cfa22
commit: 1b2de89bce7eee3c63ce2286f071db57cd2cfa22
branch: main
author: Charles Machalow <csm10495 at gmail.com>
committer: zooba <steve.dower at microsoft.com>
date: 2022-11-22T17:19:34Z
summary:

gh-99547: Add isjunction methods for checking if a path is a junction (GH-99548)

files:
A Misc/NEWS.d/next/Core and Builtins/2022-11-16-21-35-30.gh-issue-99547.p_c_bp.rst
M Doc/library/os.path.rst
M Doc/library/os.rst
M Doc/library/pathlib.rst
M Doc/whatsnew/3.12.rst
M Lib/ntpath.py
M Lib/pathlib.py
M Lib/posixpath.py
M Lib/shutil.py
M Lib/test/test_ntpath.py
M Lib/test/test_os.py
M Lib/test/test_pathlib.py
M Lib/test/test_posixpath.py
M Modules/clinic/posixmodule.c.h
M Modules/posixmodule.c

diff --git a/Doc/library/os.path.rst b/Doc/library/os.path.rst
index 6d52a03ba957..50e089653fe7 100644
--- a/Doc/library/os.path.rst
+++ b/Doc/library/os.path.rst
@@ -266,6 +266,15 @@ the :mod:`glob` module.)
       Accepts a :term:`path-like object`.
 
 
+.. function:: isjunction(path)
+
+   Return ``True`` if *path* refers to an :func:`existing <lexists>` directory
+   entry that is a junction.  Always return ``False`` if junctions are not
+   supported on the current platform.
+
+   .. versionadded:: 3.12
+
+
 .. function:: islink(path)
 
    Return ``True`` if *path* refers to an :func:`existing <exists>` directory
diff --git a/Doc/library/os.rst b/Doc/library/os.rst
index 3387d0842da8..775aa32df99a 100644
--- a/Doc/library/os.rst
+++ b/Doc/library/os.rst
@@ -2738,6 +2738,17 @@ features:
       This method can raise :exc:`OSError`, such as :exc:`PermissionError`,
       but :exc:`FileNotFoundError` is caught and not raised.
 
+   .. method:: is_junction()
+
+      Return ``True`` if this entry is a junction (even if broken);
+      return ``False`` if the entry points to a regular directory, any kind
+      of file, a symlink, or if it doesn't exist anymore.
+
+      The result is cached on the ``os.DirEntry`` object. Call
+      :func:`os.path.isjunction` to fetch up-to-date information.
+
+      .. versionadded:: 3.12
+
    .. method:: stat(*, follow_symlinks=True)
 
       Return a :class:`stat_result` object for this entry. This method
@@ -2760,8 +2771,8 @@ features:
    Note that there is a nice correspondence between several attributes
    and methods of ``os.DirEntry`` and of :class:`pathlib.Path`.  In
    particular, the ``name`` attribute has the same
-   meaning, as do the ``is_dir()``, ``is_file()``, ``is_symlink()``
-   and ``stat()`` methods.
+   meaning, as do the ``is_dir()``, ``is_file()``, ``is_symlink()``,
+   ``is_junction()``, and ``stat()`` methods.
 
    .. versionadded:: 3.5
 
diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst
index 944963e1e1ae..6537637f33c7 100644
--- a/Doc/library/pathlib.rst
+++ b/Doc/library/pathlib.rst
@@ -891,6 +891,14 @@ call fails (for example because the path doesn't exist).
    other errors (such as permission errors) are propagated.
 
 
+.. method:: Path.is_junction()
+
+   Return ``True`` if the path points to a junction, and ``False`` for any other
+   type of file. Currently only Windows supports junctions.
+
+   .. versionadded:: 3.12
+
+
 .. method:: Path.is_mount()
 
    Return ``True`` if the path is a :dfn:`mount point`: a point in a
diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst
index 8e9a4f04a890..a9b69c2ebf43 100644
--- a/Doc/whatsnew/3.12.rst
+++ b/Doc/whatsnew/3.12.rst
@@ -234,6 +234,10 @@ pathlib
   more consistent with :func:`os.path.relpath`.
   (Contributed by Domenico Ragusa in :issue:`40358`.)
 
+* Add :meth:`pathlib.Path.is_junction` as a proxy to :func:`os.path.isjunction`.
+  (Contributed by Charles Machalow in :gh:`99547`.)
+
+
 dis
 ---
 
@@ -252,6 +256,14 @@ os
   for a process with :func:`os.pidfd_open` in non-blocking mode.
   (Contributed by Kumar Aditya in :gh:`93312`.)
 
+* Add :func:`os.path.isjunction` to check if a given path is a junction.
+  (Contributed by Charles Machalow in :gh:`99547`.)
+
+* :class:`os.DirEntry` now includes an :meth:`os.DirEntry.is_junction`
+  method to check if the entry is a junction.
+  (Contributed by Charles Machalow in :gh:`99547`.)
+
+
 shutil
 ------
 
diff --git a/Lib/ntpath.py b/Lib/ntpath.py
index d9582f408743..873c884c3bd9 100644
--- a/Lib/ntpath.py
+++ b/Lib/ntpath.py
@@ -30,7 +30,7 @@
            "ismount", "expanduser","expandvars","normpath","abspath",
            "curdir","pardir","sep","pathsep","defpath","altsep",
            "extsep","devnull","realpath","supports_unicode_filenames","relpath",
-           "samefile", "sameopenfile", "samestat", "commonpath"]
+           "samefile", "sameopenfile", "samestat", "commonpath", "isjunction"]
 
 def _get_bothseps(path):
     if isinstance(path, bytes):
@@ -267,6 +267,24 @@ def islink(path):
         return False
     return stat.S_ISLNK(st.st_mode)
 
+
+# Is a path a junction?
+
+if hasattr(os.stat_result, 'st_reparse_tag'):
+    def isjunction(path):
+        """Test whether a path is a junction"""
+        try:
+            st = os.lstat(path)
+        except (OSError, ValueError, AttributeError):
+            return False
+        return bool(st.st_reparse_tag == stat.IO_REPARSE_TAG_MOUNT_POINT)
+else:
+    def isjunction(path):
+        """Test whether a path is a junction"""
+        os.fspath(path)
+        return False
+
+
 # Being true for dangling symbolic links is also useful.
 
 def lexists(path):
diff --git a/Lib/pathlib.py b/Lib/pathlib.py
index 068d1b02f484..bc57ae60e725 100644
--- a/Lib/pathlib.py
+++ b/Lib/pathlib.py
@@ -1223,6 +1223,12 @@ def is_symlink(self):
             # Non-encodable path
             return False
 
+    def is_junction(self):
+        """
+        Whether this path is a junction.
+        """
+        return self._flavour.pathmod.isjunction(self)
+
     def is_block_device(self):
         """
         Whether this path is a block device.
diff --git a/Lib/posixpath.py b/Lib/posixpath.py
index 5b4d78bca061..737f8a5c156d 100644
--- a/Lib/posixpath.py
+++ b/Lib/posixpath.py
@@ -35,7 +35,7 @@
            "samefile","sameopenfile","samestat",
            "curdir","pardir","sep","pathsep","defpath","altsep","extsep",
            "devnull","realpath","supports_unicode_filenames","relpath",
-           "commonpath"]
+           "commonpath", "isjunction"]
 
 
 def _get_sep(path):
@@ -169,6 +169,16 @@ def islink(path):
         return False
     return stat.S_ISLNK(st.st_mode)
 
+
+# Is a path a junction?
+
+def isjunction(path):
+    """Test whether a path is a junction
+    Junctions are not a part of posix semantics"""
+    os.fspath(path)
+    return False
+
+
 # Being true for dangling symbolic links is also useful.
 
 def lexists(path):
diff --git a/Lib/shutil.py b/Lib/shutil.py
index f5687e3b346e..f372406a6c51 100644
--- a/Lib/shutil.py
+++ b/Lib/shutil.py
@@ -565,18 +565,6 @@ def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2,
                      dirs_exist_ok=dirs_exist_ok)
 
 if hasattr(os.stat_result, 'st_file_attributes'):
-    # Special handling for directory junctions to make them behave like
-    # symlinks for shutil.rmtree, since in general they do not appear as
-    # regular links.
-    def _rmtree_isdir(entry):
-        try:
-            st = entry.stat(follow_symlinks=False)
-            return (stat.S_ISDIR(st.st_mode) and not
-                (st.st_file_attributes & stat.FILE_ATTRIBUTE_REPARSE_POINT
-                 and st.st_reparse_tag == stat.IO_REPARSE_TAG_MOUNT_POINT))
-        except OSError:
-            return False
-
     def _rmtree_islink(path):
         try:
             st = os.lstat(path)
@@ -586,12 +574,6 @@ def _rmtree_islink(path):
         except OSError:
             return False
 else:
-    def _rmtree_isdir(entry):
-        try:
-            return entry.is_dir(follow_symlinks=False)
-        except OSError:
-            return False
-
     def _rmtree_islink(path):
         return os.path.islink(path)
 
@@ -605,7 +587,12 @@ def _rmtree_unsafe(path, onerror):
         entries = []
     for entry in entries:
         fullname = entry.path
-        if _rmtree_isdir(entry):
+        try:
+            is_dir = entry.is_dir(follow_symlinks=False)
+        except OSError:
+            is_dir = False
+
+        if is_dir and not entry.is_junction():
             try:
                 if entry.is_symlink():
                     # This can only happen if someone replaces
diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py
index d51946322c80..336648273b6c 100644
--- a/Lib/test/test_ntpath.py
+++ b/Lib/test/test_ntpath.py
@@ -856,6 +856,23 @@ def test_nt_helpers(self):
             self.assertIsInstance(b_final_path, bytes)
             self.assertGreater(len(b_final_path), 0)
 
+    @unittest.skipIf(sys.platform != 'win32', "Can only test junctions with creation on win32.")
+    def test_isjunction(self):
+        with os_helper.temp_dir() as d:
+            with os_helper.change_cwd(d):
+                os.mkdir('tmpdir')
+
+                import _winapi
+                try:
+                    _winapi.CreateJunction('tmpdir', 'testjunc')
+                except OSError:
+                    raise unittest.SkipTest('creating the test junction failed')
+
+                self.assertTrue(ntpath.isjunction('testjunc'))
+                self.assertFalse(ntpath.isjunction('tmpdir'))
+                self.assertPathEqual(ntpath.realpath('testjunc'), ntpath.realpath('tmpdir'))
+
+
 class NtCommonTest(test_genericpath.CommonTest, unittest.TestCase):
     pathmodule = ntpath
     attributes = ['relpath']
diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py
index cb78e8cb77de..94db8bb7737a 100644
--- a/Lib/test/test_os.py
+++ b/Lib/test/test_os.py
@@ -4158,6 +4158,8 @@ def check_entry(self, entry, name, is_dir, is_file, is_symlink):
         self.assertEqual(entry.is_file(follow_symlinks=False),
                          stat.S_ISREG(entry_lstat.st_mode))
 
+        self.assertEqual(entry.is_junction(), os.path.isjunction(entry.path))
+
         self.assert_stat_equal(entry.stat(),
                                entry_stat,
                                os.name == 'nt' and not is_symlink)
@@ -4206,6 +4208,21 @@ def test_attributes(self):
             entry = entries['symlink_file.txt']
             self.check_entry(entry, 'symlink_file.txt', False, True, True)
 
+    @unittest.skipIf(sys.platform != 'win32', "Can only test junctions with creation on win32.")
+    def test_attributes_junctions(self):
+        dirname = os.path.join(self.path, "tgtdir")
+        os.mkdir(dirname)
+
+        import _winapi
+        try:
+            _winapi.CreateJunction(dirname, os.path.join(self.path, "srcjunc"))
+        except OSError:
+            raise unittest.SkipTest('creating the test junction failed')
+
+        entries = self.get_entries(['srcjunc', 'tgtdir'])
+        self.assertEqual(entries['srcjunc'].is_junction(), True)
+        self.assertEqual(entries['tgtdir'].is_junction(), False)
+
     def get_entry(self, name):
         path = self.bytes_path if isinstance(name, bytes) else self.path
         entries = list(os.scandir(path))
diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py
index 3b1f302cc964..94401e5429cd 100644
--- a/Lib/test/test_pathlib.py
+++ b/Lib/test/test_pathlib.py
@@ -2411,6 +2411,13 @@ def test_is_symlink(self):
             self.assertIs((P / 'linkA\udfff').is_file(), False)
             self.assertIs((P / 'linkA\x00').is_file(), False)
 
+    def test_is_junction(self):
+        P = self.cls(BASE)
+
+        with mock.patch.object(P._flavour, 'pathmod'):
+            self.assertEqual(P.is_junction(), P._flavour.pathmod.isjunction.return_value)
+            P._flavour.pathmod.isjunction.assert_called_once_with(P)
+
     def test_is_fifo_false(self):
         P = self.cls(BASE)
         self.assertFalse((P / 'fileA').is_fifo())
diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py
index 8a1dd131928c..6c1c0f5577b7 100644
--- a/Lib/test/test_posixpath.py
+++ b/Lib/test/test_posixpath.py
@@ -244,6 +244,9 @@ def fake_lstat(path):
         finally:
             os.lstat = save_lstat
 
+    def test_isjunction(self):
+        self.assertFalse(posixpath.isjunction(ABSTFN))
+
     def test_expanduser(self):
         self.assertEqual(posixpath.expanduser("foo"), "foo")
         self.assertEqual(posixpath.expanduser(b"foo"), b"foo")
diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-11-16-21-35-30.gh-issue-99547.p_c_bp.rst b/Misc/NEWS.d/next/Core and Builtins/2022-11-16-21-35-30.gh-issue-99547.p_c_bp.rst
new file mode 100644
index 000000000000..7e3c52924213
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2022-11-16-21-35-30.gh-issue-99547.p_c_bp.rst	
@@ -0,0 +1 @@
+Add a function to os.path to check if a path is a junction: isjunction. Add similar functionality to pathlib.Path as is_junction.
diff --git a/Modules/clinic/posixmodule.c.h b/Modules/clinic/posixmodule.c.h
index 1ad96ea296ea..f9f6ca372ec6 100644
--- a/Modules/clinic/posixmodule.c.h
+++ b/Modules/clinic/posixmodule.c.h
@@ -10269,6 +10269,38 @@ os_DirEntry_is_symlink(DirEntry *self, PyTypeObject *defining_class, PyObject *c
     return return_value;
 }
 
+PyDoc_STRVAR(os_DirEntry_is_junction__doc__,
+"is_junction($self, /)\n"
+"--\n"
+"\n"
+"Return True if the entry is a junction; cached per entry.");
+
+#define OS_DIRENTRY_IS_JUNCTION_METHODDEF    \
+    {"is_junction", _PyCFunction_CAST(os_DirEntry_is_junction), METH_METHOD|METH_FASTCALL|METH_KEYWORDS, os_DirEntry_is_junction__doc__},
+
+static int
+os_DirEntry_is_junction_impl(DirEntry *self, PyTypeObject *defining_class);
+
+static PyObject *
+os_DirEntry_is_junction(DirEntry *self, PyTypeObject *defining_class, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
+{
+    PyObject *return_value = NULL;
+    int _return_value;
+
+    if (nargs) {
+        PyErr_SetString(PyExc_TypeError, "is_junction() takes no arguments");
+        goto exit;
+    }
+    _return_value = os_DirEntry_is_junction_impl(self, defining_class);
+    if ((_return_value == -1) && PyErr_Occurred()) {
+        goto exit;
+    }
+    return_value = PyBool_FromLong((long)_return_value);
+
+exit:
+    return return_value;
+}
+
 PyDoc_STRVAR(os_DirEntry_stat__doc__,
 "stat($self, /, *, follow_symlinks=True)\n"
 "--\n"
@@ -11517,4 +11549,4 @@ os_waitstatus_to_exitcode(PyObject *module, PyObject *const *args, Py_ssize_t na
 #ifndef OS_WAITSTATUS_TO_EXITCODE_METHODDEF
     #define OS_WAITSTATUS_TO_EXITCODE_METHODDEF
 #endif /* !defined(OS_WAITSTATUS_TO_EXITCODE_METHODDEF) */
-/*[clinic end generated code: output=90f5e6995114e5ca input=a9049054013a1b77]*/
+/*[clinic end generated code: output=4192d8e09e216300 input=a9049054013a1b77]*/
diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c
index 98fc264aff6b..45e71ee9c059 100644
--- a/Modules/posixmodule.c
+++ b/Modules/posixmodule.c
@@ -13633,6 +13633,25 @@ os_DirEntry_is_symlink_impl(DirEntry *self, PyTypeObject *defining_class)
 #endif
 }
 
+/*[clinic input]
+os.DirEntry.is_junction -> bool
+    defining_class: defining_class
+    /
+
+Return True if the entry is a junction; cached per entry.
+[clinic start generated code]*/
+
+static int
+os_DirEntry_is_junction_impl(DirEntry *self, PyTypeObject *defining_class)
+/*[clinic end generated code: output=7061a07b0ef2cd1f input=475cd36fb7d4723f]*/
+{
+#ifdef MS_WINDOWS
+    return self->win32_lstat.st_reparse_tag == IO_REPARSE_TAG_MOUNT_POINT;
+#else
+    return 0;
+#endif
+}
+
 static PyObject *
 DirEntry_fetch_stat(PyObject *module, DirEntry *self, int follow_symlinks)
 {
@@ -13927,6 +13946,7 @@ static PyMethodDef DirEntry_methods[] = {
     OS_DIRENTRY_IS_DIR_METHODDEF
     OS_DIRENTRY_IS_FILE_METHODDEF
     OS_DIRENTRY_IS_SYMLINK_METHODDEF
+    OS_DIRENTRY_IS_JUNCTION_METHODDEF
     OS_DIRENTRY_STAT_METHODDEF
     OS_DIRENTRY_INODE_METHODDEF
     OS_DIRENTRY___FSPATH___METHODDEF



More information about the Python-checkins mailing list