bpo-39950: add `pathlib.Path.hardlink_to()` method that supersedes `link_to()` (GH-18909)

The argument order of `link_to()` is reversed compared to what one may expect, so:

    a.link_to(b)

Might be expected to create *a* as a link to *b*, in fact it creates *b* as a link to *a*, making it function more like a "link from". This doesn't match `symlink_to()` nor the documentation and doesn't seem to be the original author's intent.

This PR deprecates `link_to()` and introduces `hardlink_to()`, which has the same argument order as `symlink_to()`.
This commit is contained in:
Barney Gale 2021-04-23 21:48:52 +01:00 committed by GitHub
parent e047239eaf
commit f24e2e5464
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 64 additions and 4 deletions

View File

@ -1140,6 +1140,15 @@ call fails (for example because the path doesn't exist).
The order of arguments (link, target) is the reverse The order of arguments (link, target) is the reverse
of :func:`os.symlink`'s. of :func:`os.symlink`'s.
.. method:: Path.hardlink_to(target)
Make this path a hard link to the same file as *target*.
.. note::
The order of arguments (link, target) is the reverse
of :func:`os.link`'s.
.. versionadded:: 3.10
.. method:: Path.link_to(target) .. method:: Path.link_to(target)
@ -1149,11 +1158,17 @@ call fails (for example because the path doesn't exist).
This function does not make this path a hard link to *target*, despite This function does not make this path a hard link to *target*, despite
the implication of the function and argument names. The argument order the implication of the function and argument names. The argument order
(target, link) is the reverse of :func:`Path.symlink_to`, but matches (target, link) is the reverse of :func:`Path.symlink_to` and
that of :func:`os.link`. :func:`Path.hardlink_to`, but matches that of :func:`os.link`.
.. versionadded:: 3.8 .. versionadded:: 3.8
.. deprecated:: 3.10
This method is deprecated in favor of :meth:`Path.hardlink_to`, as the
argument order of :meth:`Path.link_to` does not match that of
:meth:`Path.symlink_to`.
.. method:: Path.touch(mode=0o666, exist_ok=True) .. method:: Path.touch(mode=0o666, exist_ok=True)
@ -1246,7 +1261,7 @@ Below is a table mapping various :mod:`os` functions to their corresponding
:func:`os.path.isdir` :meth:`Path.is_dir` :func:`os.path.isdir` :meth:`Path.is_dir`
:func:`os.path.isfile` :meth:`Path.is_file` :func:`os.path.isfile` :meth:`Path.is_file`
:func:`os.path.islink` :meth:`Path.is_symlink` :func:`os.path.islink` :meth:`Path.is_symlink`
:func:`os.link` :meth:`Path.link_to` :func:`os.link` :meth:`Path.hardlink_to`
:func:`os.symlink` :meth:`Path.symlink_to` :func:`os.symlink` :meth:`Path.symlink_to`
:func:`os.readlink` :meth:`Path.readlink` :func:`os.readlink` :meth:`Path.readlink`
:func:`os.path.relpath` :meth:`Path.relative_to` [#]_ :func:`os.path.relpath` :meth:`Path.relative_to` [#]_

View File

@ -1007,6 +1007,11 @@ Added negative indexing support to :attr:`PurePath.parents
<pathlib.PurePath.parents>`. <pathlib.PurePath.parents>`.
(Contributed by Yaroslav Pankovych in :issue:`21041`) (Contributed by Yaroslav Pankovych in :issue:`21041`)
Added :meth:`Path.hardlink_to <pathlib.Path.hardlink_to>` method that
supersedes :meth:`~pathlib.Path.link_to`. The new method has the same argument
order as :meth:`~pathlib.Path.symlink_to`.
(Contributed by Barney Gale in :issue:`39950`.)
platform platform
-------- --------
@ -1363,6 +1368,10 @@ Deprecated
(Contributed by Jelle Zijlstra in :issue:`21574`.) (Contributed by Jelle Zijlstra in :issue:`21574`.)
* :meth:`pathlib.Path.link_to` is deprecated and slated for removal in
Python 3.12. Use :meth:`pathlib.Path.hardlink_to` instead.
(Contributed by Barney Gale in :issue:`39950`.)
Removed Removed
======= =======

View File

@ -6,6 +6,7 @@ import os
import posixpath import posixpath
import re import re
import sys import sys
import warnings
from _collections_abc import Sequence from _collections_abc import Sequence
from errno import EINVAL, ENOENT, ENOTDIR, EBADF, ELOOP from errno import EINVAL, ENOENT, ENOTDIR, EBADF, ELOOP
from operator import attrgetter from operator import attrgetter
@ -1306,6 +1307,14 @@ class Path(PurePath):
""" """
self._accessor.symlink(target, self, target_is_directory) self._accessor.symlink(target, self, target_is_directory)
def hardlink_to(self, target):
"""
Make this path a hard link pointing to the same file as *target*.
Note the order of arguments (self, target) is the reverse of os.link's.
"""
self._accessor.link(target, self)
def link_to(self, target): def link_to(self, target):
""" """
Make the target path a hard link pointing to this path. Make the target path a hard link pointing to this path.
@ -1315,7 +1324,13 @@ class Path(PurePath):
of arguments (target, link) is the reverse of Path.symlink_to, but of arguments (target, link) is the reverse of Path.symlink_to, but
matches that of os.link. matches that of os.link.
Deprecated since Python 3.10 and scheduled for removal in Python 3.12.
Use `hardlink_to()` instead.
""" """
warnings.warn("pathlib.Path.link_to() is deprecated and is scheduled "
"for removal in Python 3.12. "
"Use pathlib.Path.hardlink_to() instead.",
DeprecationWarning)
self._accessor.link(self, target) self._accessor.link(self, target)
# Convenience functions for querying the stat results # Convenience functions for querying the stat results

View File

@ -1925,7 +1925,8 @@ class _BasePathTest(object):
# linking to another path. # linking to another path.
q = P / 'dirA' / 'fileAA' q = P / 'dirA' / 'fileAA'
try: try:
p.link_to(q) with self.assertWarns(DeprecationWarning):
p.link_to(q)
except PermissionError as e: except PermissionError as e:
self.skipTest('os.link(): %s' % e) self.skipTest('os.link(): %s' % e)
self.assertEqual(q.stat().st_size, size) self.assertEqual(q.stat().st_size, size)
@ -1937,6 +1938,24 @@ class _BasePathTest(object):
self.assertEqual(os.stat(r).st_size, size) self.assertEqual(os.stat(r).st_size, size)
self.assertTrue(q.stat) self.assertTrue(q.stat)
@unittest.skipUnless(hasattr(os, "link"), "os.link() is not present")
def test_hardlink_to(self):
P = self.cls(BASE)
target = P / 'fileA'
size = target.stat().st_size
# linking to another path.
link = P / 'dirA' / 'fileAA'
link.hardlink_to(target)
self.assertEqual(link.stat().st_size, size)
self.assertTrue(os.path.samefile(target, link))
self.assertTrue(target.exists())
# Linking to a str of a relative path.
link2 = P / 'dirA' / 'fileAAA'
target2 = rel_join('fileA')
link2.hardlink_to(target2)
self.assertEqual(os.stat(target2).st_size, size)
self.assertTrue(link2.exists())
@unittest.skipIf(hasattr(os, "link"), "os.link() is present") @unittest.skipIf(hasattr(os, "link"), "os.link() is present")
def test_link_to_not_implemented(self): def test_link_to_not_implemented(self):
P = self.cls(BASE) P = self.cls(BASE)

View File

@ -0,0 +1,2 @@
Add `pathlib.Path.hardlink_to()` method that supersedes `link_to()`. The new
method has the same argument order as `symlink_to()`.