Skip to content

Commit 1b2de89

Browse files
authored
gh-99547: Add isjunction methods for checking if a path is a junction (GH-99548)
1 parent c210213 commit 1b2de89

15 files changed

+182
-24
lines changed

Doc/library/os.path.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,15 @@ the :mod:`glob` module.)
266266
Accepts a :term:`path-like object`.
267267

268268

269+
.. function:: isjunction(path)
270+
271+
Return ``True`` if *path* refers to an :func:`existing <lexists>` directory
272+
entry that is a junction. Always return ``False`` if junctions are not
273+
supported on the current platform.
274+
275+
.. versionadded:: 3.12
276+
277+
269278
.. function:: islink(path)
270279

271280
Return ``True`` if *path* refers to an :func:`existing <exists>` directory

Doc/library/os.rst

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2738,6 +2738,17 @@ features:
27382738
This method can raise :exc:`OSError`, such as :exc:`PermissionError`,
27392739
but :exc:`FileNotFoundError` is caught and not raised.
27402740

2741+
.. method:: is_junction()
2742+
2743+
Return ``True`` if this entry is a junction (even if broken);
2744+
return ``False`` if the entry points to a regular directory, any kind
2745+
of file, a symlink, or if it doesn't exist anymore.
2746+
2747+
The result is cached on the ``os.DirEntry`` object. Call
2748+
:func:`os.path.isjunction` to fetch up-to-date information.
2749+
2750+
.. versionadded:: 3.12
2751+
27412752
.. method:: stat(*, follow_symlinks=True)
27422753

27432754
Return a :class:`stat_result` object for this entry. This method
@@ -2760,8 +2771,8 @@ features:
27602771
Note that there is a nice correspondence between several attributes
27612772
and methods of ``os.DirEntry`` and of :class:`pathlib.Path`. In
27622773
particular, the ``name`` attribute has the same
2763-
meaning, as do the ``is_dir()``, ``is_file()``, ``is_symlink()``
2764-
and ``stat()`` methods.
2774+
meaning, as do the ``is_dir()``, ``is_file()``, ``is_symlink()``,
2775+
``is_junction()``, and ``stat()`` methods.
27652776

27662777
.. versionadded:: 3.5
27672778

Doc/library/pathlib.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,14 @@ call fails (for example because the path doesn't exist).
891891
other errors (such as permission errors) are propagated.
892892

893893

894+
.. method:: Path.is_junction()
895+
896+
Return ``True`` if the path points to a junction, and ``False`` for any other
897+
type of file. Currently only Windows supports junctions.
898+
899+
.. versionadded:: 3.12
900+
901+
894902
.. method:: Path.is_mount()
895903

896904
Return ``True`` if the path is a :dfn:`mount point`: a point in a

Doc/whatsnew/3.12.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,10 @@ pathlib
234234
more consistent with :func:`os.path.relpath`.
235235
(Contributed by Domenico Ragusa in :issue:`40358`.)
236236

237+
* Add :meth:`pathlib.Path.is_junction` as a proxy to :func:`os.path.isjunction`.
238+
(Contributed by Charles Machalow in :gh:`99547`.)
239+
240+
237241
dis
238242
---
239243

@@ -252,6 +256,14 @@ os
252256
for a process with :func:`os.pidfd_open` in non-blocking mode.
253257
(Contributed by Kumar Aditya in :gh:`93312`.)
254258

259+
* Add :func:`os.path.isjunction` to check if a given path is a junction.
260+
(Contributed by Charles Machalow in :gh:`99547`.)
261+
262+
* :class:`os.DirEntry` now includes an :meth:`os.DirEntry.is_junction`
263+
method to check if the entry is a junction.
264+
(Contributed by Charles Machalow in :gh:`99547`.)
265+
266+
255267
shutil
256268
------
257269

Lib/ntpath.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"ismount", "expanduser","expandvars","normpath","abspath",
3131
"curdir","pardir","sep","pathsep","defpath","altsep",
3232
"extsep","devnull","realpath","supports_unicode_filenames","relpath",
33-
"samefile", "sameopenfile", "samestat", "commonpath"]
33+
"samefile", "sameopenfile", "samestat", "commonpath", "isjunction"]
3434

3535
def _get_bothseps(path):
3636
if isinstance(path, bytes):
@@ -267,6 +267,24 @@ def islink(path):
267267
return False
268268
return stat.S_ISLNK(st.st_mode)
269269

270+
271+
# Is a path a junction?
272+
273+
if hasattr(os.stat_result, 'st_reparse_tag'):
274+
def isjunction(path):
275+
"""Test whether a path is a junction"""
276+
try:
277+
st = os.lstat(path)
278+
except (OSError, ValueError, AttributeError):
279+
return False
280+
return bool(st.st_reparse_tag == stat.IO_REPARSE_TAG_MOUNT_POINT)
281+
else:
282+
def isjunction(path):
283+
"""Test whether a path is a junction"""
284+
os.fspath(path)
285+
return False
286+
287+
270288
# Being true for dangling symbolic links is also useful.
271289

272290
def lexists(path):

Lib/pathlib.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,6 +1223,12 @@ def is_symlink(self):
12231223
# Non-encodable path
12241224
return False
12251225

1226+
def is_junction(self):
1227+
"""
1228+
Whether this path is a junction.
1229+
"""
1230+
return self._flavour.pathmod.isjunction(self)
1231+
12261232
def is_block_device(self):
12271233
"""
12281234
Whether this path is a block device.

Lib/posixpath.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
"samefile","sameopenfile","samestat",
3636
"curdir","pardir","sep","pathsep","defpath","altsep","extsep",
3737
"devnull","realpath","supports_unicode_filenames","relpath",
38-
"commonpath"]
38+
"commonpath", "isjunction"]
3939

4040

4141
def _get_sep(path):
@@ -169,6 +169,16 @@ def islink(path):
169169
return False
170170
return stat.S_ISLNK(st.st_mode)
171171

172+
173+
# Is a path a junction?
174+
175+
def isjunction(path):
176+
"""Test whether a path is a junction
177+
Junctions are not a part of posix semantics"""
178+
os.fspath(path)
179+
return False
180+
181+
172182
# Being true for dangling symbolic links is also useful.
173183

174184
def lexists(path):

Lib/shutil.py

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -565,18 +565,6 @@ def copytree(src, dst, symlinks=False, ignore=None, copy_function=copy2,
565565
dirs_exist_ok=dirs_exist_ok)
566566

567567
if hasattr(os.stat_result, 'st_file_attributes'):
568-
# Special handling for directory junctions to make them behave like
569-
# symlinks for shutil.rmtree, since in general they do not appear as
570-
# regular links.
571-
def _rmtree_isdir(entry):
572-
try:
573-
st = entry.stat(follow_symlinks=False)
574-
return (stat.S_ISDIR(st.st_mode) and not
575-
(st.st_file_attributes & stat.FILE_ATTRIBUTE_REPARSE_POINT
576-
and st.st_reparse_tag == stat.IO_REPARSE_TAG_MOUNT_POINT))
577-
except OSError:
578-
return False
579-
580568
def _rmtree_islink(path):
581569
try:
582570
st = os.lstat(path)
@@ -586,12 +574,6 @@ def _rmtree_islink(path):
586574
except OSError:
587575
return False
588576
else:
589-
def _rmtree_isdir(entry):
590-
try:
591-
return entry.is_dir(follow_symlinks=False)
592-
except OSError:
593-
return False
594-
595577
def _rmtree_islink(path):
596578
return os.path.islink(path)
597579

@@ -605,7 +587,12 @@ def _rmtree_unsafe(path, onerror):
605587
entries = []
606588
for entry in entries:
607589
fullname = entry.path
608-
if _rmtree_isdir(entry):
590+
try:
591+
is_dir = entry.is_dir(follow_symlinks=False)
592+
except OSError:
593+
is_dir = False
594+
595+
if is_dir and not entry.is_junction():
609596
try:
610597
if entry.is_symlink():
611598
# This can only happen if someone replaces

Lib/test/test_ntpath.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,23 @@ def test_nt_helpers(self):
856856
self.assertIsInstance(b_final_path, bytes)
857857
self.assertGreater(len(b_final_path), 0)
858858

859+
@unittest.skipIf(sys.platform != 'win32', "Can only test junctions with creation on win32.")
860+
def test_isjunction(self):
861+
with os_helper.temp_dir() as d:
862+
with os_helper.change_cwd(d):
863+
os.mkdir('tmpdir')
864+
865+
import _winapi
866+
try:
867+
_winapi.CreateJunction('tmpdir', 'testjunc')
868+
except OSError:
869+
raise unittest.SkipTest('creating the test junction failed')
870+
871+
self.assertTrue(ntpath.isjunction('testjunc'))
872+
self.assertFalse(ntpath.isjunction('tmpdir'))
873+
self.assertPathEqual(ntpath.realpath('testjunc'), ntpath.realpath('tmpdir'))
874+
875+
859876
class NtCommonTest(test_genericpath.CommonTest, unittest.TestCase):
860877
pathmodule = ntpath
861878
attributes = ['relpath']

Lib/test/test_os.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4158,6 +4158,8 @@ def check_entry(self, entry, name, is_dir, is_file, is_symlink):
41584158
self.assertEqual(entry.is_file(follow_symlinks=False),
41594159
stat.S_ISREG(entry_lstat.st_mode))
41604160

4161+
self.assertEqual(entry.is_junction(), os.path.isjunction(entry.path))
4162+
41614163
self.assert_stat_equal(entry.stat(),
41624164
entry_stat,
41634165
os.name == 'nt' and not is_symlink)
@@ -4206,6 +4208,21 @@ def test_attributes(self):
42064208
entry = entries['symlink_file.txt']
42074209
self.check_entry(entry, 'symlink_file.txt', False, True, True)
42084210

4211+
@unittest.skipIf(sys.platform != 'win32', "Can only test junctions with creation on win32.")
4212+
def test_attributes_junctions(self):
4213+
dirname = os.path.join(self.path, "tgtdir")
4214+
os.mkdir(dirname)
4215+
4216+
import _winapi
4217+
try:
4218+
_winapi.CreateJunction(dirname, os.path.join(self.path, "srcjunc"))
4219+
except OSError:
4220+
raise unittest.SkipTest('creating the test junction failed')
4221+
4222+
entries = self.get_entries(['srcjunc', 'tgtdir'])
4223+
self.assertEqual(entries['srcjunc'].is_junction(), True)
4224+
self.assertEqual(entries['tgtdir'].is_junction(), False)
4225+
42094226
def get_entry(self, name):
42104227
path = self.bytes_path if isinstance(name, bytes) else self.path
42114228
entries = list(os.scandir(path))

Lib/test/test_pathlib.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2411,6 +2411,13 @@ def test_is_symlink(self):
24112411
self.assertIs((P / 'linkA\udfff').is_file(), False)
24122412
self.assertIs((P / 'linkA\x00').is_file(), False)
24132413

2414+
def test_is_junction(self):
2415+
P = self.cls(BASE)
2416+
2417+
with mock.patch.object(P._flavour, 'pathmod'):
2418+
self.assertEqual(P.is_junction(), P._flavour.pathmod.isjunction.return_value)
2419+
P._flavour.pathmod.isjunction.assert_called_once_with(P)
2420+
24142421
def test_is_fifo_false(self):
24152422
P = self.cls(BASE)
24162423
self.assertFalse((P / 'fileA').is_fifo())

Lib/test/test_posixpath.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,9 @@ def fake_lstat(path):
244244
finally:
245245
os.lstat = save_lstat
246246

247+
def test_isjunction(self):
248+
self.assertFalse(posixpath.isjunction(ABSTFN))
249+
247250
def test_expanduser(self):
248251
self.assertEqual(posixpath.expanduser("foo"), "foo")
249252
self.assertEqual(posixpath.expanduser(b"foo"), b"foo")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.

Modules/clinic/posixmodule.c.h

Lines changed: 33 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/posixmodule.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13633,6 +13633,25 @@ os_DirEntry_is_symlink_impl(DirEntry *self, PyTypeObject *defining_class)
1363313633
#endif
1363413634
}
1363513635

13636+
/*[clinic input]
13637+
os.DirEntry.is_junction -> bool
13638+
defining_class: defining_class
13639+
/
13640+
13641+
Return True if the entry is a junction; cached per entry.
13642+
[clinic start generated code]*/
13643+
13644+
static int
13645+
os_DirEntry_is_junction_impl(DirEntry *self, PyTypeObject *defining_class)
13646+
/*[clinic end generated code: output=7061a07b0ef2cd1f input=475cd36fb7d4723f]*/
13647+
{
13648+
#ifdef MS_WINDOWS
13649+
return self->win32_lstat.st_reparse_tag == IO_REPARSE_TAG_MOUNT_POINT;
13650+
#else
13651+
return 0;
13652+
#endif
13653+
}
13654+
1363613655
static PyObject *
1363713656
DirEntry_fetch_stat(PyObject *module, DirEntry *self, int follow_symlinks)
1363813657
{
@@ -13927,6 +13946,7 @@ static PyMethodDef DirEntry_methods[] = {
1392713946
OS_DIRENTRY_IS_DIR_METHODDEF
1392813947
OS_DIRENTRY_IS_FILE_METHODDEF
1392913948
OS_DIRENTRY_IS_SYMLINK_METHODDEF
13949+
OS_DIRENTRY_IS_JUNCTION_METHODDEF
1393013950
OS_DIRENTRY_STAT_METHODDEF
1393113951
OS_DIRENTRY_INODE_METHODDEF
1393213952
OS_DIRENTRY___FSPATH___METHODDEF

0 commit comments

Comments
 (0)