Skip to content

gh-99547: Add isjunction methods for checking if a path is a junction #99548

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 44 commits into from
Nov 22, 2022
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
7b1eb2e
Add an isjunction method to ntpath and posixpath, pass it along as we…
csm10495 Nov 11, 2022
d2a769a
Add is_junction test for pathlib
csm10495 Nov 11, 2022
5744fc0
Add a skipIf not win32 for testing ntpath isjunction on non windows p…
csm10495 Nov 11, 2022
659af26
Merge branch 'python:main' into add_isjunction
csm10495 Nov 12, 2022
dc9f389
Merge branch 'python:main' into add_isjunction
csm10495 Nov 16, 2022
e54add0
📜🤖 Added by blurb_it.
blurb-it[bot] Nov 16, 2022
ccaa147
Merge branch 'python:main' into add_isjunction
csm10495 Nov 16, 2022
db3f5b3
Update Lib/test/test_ntpath.py
csm10495 Nov 17, 2022
07aaab6
remove un-needed import
csm10495 Nov 17, 2022
12b3bc9
Merge branch 'main' into add_isjunction
csm10495 Nov 17, 2022
bee6b2c
Update Lib/ntpath.py
csm10495 Nov 17, 2022
e3fbf24
fix test... this was a string not an obj
csm10495 Nov 17, 2022
262843f
Update Lib/posixpath.py
csm10495 Nov 17, 2022
1a8695f
Update Lib/ntpath.py
csm10495 Nov 17, 2022
8b32ce5
Empty commit to rekick ci
csm10495 Nov 17, 2022
a20ebea
update os.path.rst for isjunction
csm10495 Nov 17, 2022
dfdc033
update pathlib docs for is_junction
csm10495 Nov 17, 2022
a64052d
Merge pull request #1 from csm10495/add_isjunction
csm10495 Nov 17, 2022
b005344
Merge pull request #2 from csm10495/main
csm10495 Nov 17, 2022
be80054
Merge branch 'main' into add_isjunction
csm10495 Nov 17, 2022
9f8185c
Update 3.12.rst
csm10495 Nov 17, 2022
fc75452
Merge pull request #3 from csm10495/csm10495-patch-1
csm10495 Nov 17, 2022
5cd2393
Update Doc/library/os.path.rst
csm10495 Nov 17, 2022
c0366c8
Update pathlib.rst
csm10495 Nov 17, 2022
9d69989
Update pathlib.rst
csm10495 Nov 17, 2022
5ee9979
Update os.path.rst
csm10495 Nov 17, 2022
ca01686
Update 3.12.rst
csm10495 Nov 17, 2022
d036f73
Update os.path.rst
csm10495 Nov 17, 2022
a8461ea
Update Lib/ntpath.py
csm10495 Nov 18, 2022
8f92e67
pr fixes, also add contribution notices
csm10495 Nov 18, 2022
b71a0db
Swap back to LF line endings
csm10495 Nov 18, 2022
863a1d7
Merge branch 'main' into add_isjunction
csm10495 Nov 18, 2022
4a9330a
update docs
csm10495 Nov 18, 2022
aa27ead
fix mistype
csm10495 Nov 18, 2022
8b6d55d
Add test for DirEntry is_junction()
csm10495 Nov 18, 2022
f3e6483
rekick ci
csm10495 Nov 18, 2022
4e448f7
Update Doc/library/os.rst
csm10495 Nov 18, 2022
25f48be
Update Doc/library/os.rst
csm10495 Nov 18, 2022
2dea623
Update Lib/test/test_os.py
csm10495 Nov 19, 2022
5b6ee7d
kick ci
csm10495 Nov 19, 2022
dfeb40b
Remove _rmtree_is_dir() since we can now just check if the entry is a…
csm10495 Nov 22, 2022
8a90cba
Update Lib/shutil.py
csm10495 Nov 22, 2022
296992b
update from suggestion
csm10495 Nov 22, 2022
f3372db
Update Lib/shutil.py
csm10495 Nov 22, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Doc/library/os.path.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 13 additions & 2 deletions Doc/library/os.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 or 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
Expand All @@ -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

Expand Down
8 changes: 8 additions & 0 deletions Doc/library/pathlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions Doc/whatsnew/3.12.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---

Expand All @@ -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
------

Expand Down
20 changes: 19 additions & 1 deletion Lib/ntpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
6 changes: 6 additions & 0 deletions Lib/pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 11 additions & 1 deletion Lib/posixpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
17 changes: 17 additions & 0 deletions Lib/test/test_ntpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
17 changes: 17 additions & 0 deletions Lib/test/test_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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(os.path.join(self.path, "srcjunc"), dirname)
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))
Expand Down
7 changes: 7 additions & 0 deletions Lib/test/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
3 changes: 3 additions & 0 deletions Lib/test/test_posixpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
34 changes: 33 additions & 1 deletion Modules/clinic/posixmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions Modules/posixmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -13622,6 +13622,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)
{
Expand Down Expand Up @@ -13916,6 +13935,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
Expand Down