Reputation: 11405
I have a Python 2.7 program which must create a symbolic link with a modification date in the past. I can create the link with os.symlink()
, and os.utime()
claims to set the access time and modification time of files, but when I use os.utime()
on my newly-created symlink, it changes the atime and mtime of the file to which the symlink points, rather than the atime and mtime of the symbolic link.
What is the best way to set the access time and modification time of a symbolic link from Python code?
Here is a test program which demonstrates what I am doing:
#!/usr/bin/env python2.7
import os, datetime, time
if __name__ == '__main__':
path1, path2 = 'source', 'link'
if os.path.exists(path1):
os.rmdir(path1)
os.mkdir(path1)
if os.path.lexists(path2):
os.remove(path2)
os.symlink(path1, 'link')
lstat1, lstat2 = os.lstat(path1), os.lstat(path2)
print("Before: {path1} atime {sa}, mtime {sm}, {path2} atime {la}, mtime {lm}".format(
path1=path1, path2=path2, sa=lstat1.st_atime, sm=lstat1.st_mtime,
la=lstat2.st_atime, lm=lstat2.st_mtime))
long_ago = datetime.datetime(datetime.date.today().year - 1,1,1,00,00,00)
long_ago_posix = time.mktime(long_ago.timetuple())
print("Desired: {path1} unchanged, {path2} atime {m}, mtime {m}".format(
path1=path1, path2=path2, m=long_ago_posix))
os.utime(path2, (long_ago_posix, long_ago_posix))
lstat1, lstat2 = os.lstat(path1), os.lstat(path2)
print("After: {path1} atime {sa}, mtime {sm}, {path2} atime {la}, mtime {lm}".format(
path1=path1, path2=path2, sa=lstat1.st_atime, sm=lstat1.st_mtime,
la=lstat2.st_atime, lm=lstat2.st_mtime))
This is the misbehaviour I see. The "After:" times change for "source" and not for "link", but the reverse should happen:
% ../src/utime_symlink_test.py
Before: source atime 1514931280.0, mtime 1514931280.0, link atime 1514931280.0, mtime 1514931280.0
Desired: source unchanged, link atime 1483257600.0, mtime 1483257600.0
After: source atime 1483257600.0, mtime 1483257600.0, link atime 1514931280.0, mtime 1514931280.0
% ls -ldT source link
lrwxr-xr-x 1 myuser staff 6 2 Jan 14:14:40 2018 link -> source
drwxr-xr-x 2 myuser staff 68 1 Jan 00:00:00 2017 source
By contrast, touch -h
changes the atime and mtime of the symlink as I want.
% touch -h -t 201701010000 link
% ls -ldT source link
lrwxr-xr-x 1 myuser staff 6 1 Jan 00:00:00 2017 link -> source
drwxr-xr-x 2 myuser staff 68 1 Jan 00:00:00 2017 source
Maybe executing touch -h
from Python is my best choice, but I'm hoping for something better.
Upvotes: 4
Views: 1831
Reputation: 11405
As @Barmar points out, Python 3's os.utime() has a parameter, follow_symlinks = False
, which gives the behaviour the questioner wants. Unfortunately, Python 2's os.utime() does not permit this parameter.
An alternative for Python 2 is to call out to the touch
command, using subprocess.call()
. This actually works on Python 3 as well. However, I only tested it on Mac. It probably works on Linux, which has a similar touch
utility pre-installed and a similar process invocation convention. It is not tested on Windows, and may well not work there unless you go out of your way to install a touch
utility.
Here is the question's test program, rewritten to show these three options. Call it with a single argument, one of 2.utime
(fails), 3.utime
(succeeds, Python 3 only), or 2.touch
(succeeds, maybe Mac or Linux only). Default is 2.utime
.
import os, datetime, time, sys, subprocess
if __name__ == '__main__':
method = 'missing' if len(sys.argv) < 2 else sys.argv[1]
path1, path2 = 'source', 'link'
if os.path.exists(path1):
os.rmdir(path1)
os.mkdir(path1)
if os.path.lexists(path2):
os.remove(path2)
os.symlink(path1, 'link')
lstat1, lstat2 = os.lstat(path1), os.lstat(path2)
print("Before: {path1} atime {sa}, mtime {sm}, {path2} atime {la}, mtime {lm}".format(
path1=path1, path2=path2, sa=lstat1.st_atime, sm=lstat1.st_mtime,
la=lstat2.st_atime, lm=lstat2.st_mtime))
long_ago = datetime.datetime(datetime.date.today().year - 1,1,1,00,00,00)
long_ago_posix = time.mktime(long_ago.timetuple())
print("Desired: {path1} unchanged, {path2} atime {m}, mtime {m}".format(
path1=path1, path2=path2, m=long_ago_posix))
if method in ['missing', '2.utime']:
# runs on Python 2 or 3, always follows symbolic links
os.utime(path2, (long_ago_posix, long_ago_posix))
elif method in ['2.touch']:
# runs on Python 2 or 3, tested on Mac only, maybe works on Linux, probably not Windows
invocation = ['touch', '-h', '-t', long_ago.strftime('%Y%m%d%H%M.%S'), path2]
subprocess.call(invocation)
elif method in ['3.utime']:
# runs on Python 3 only, changes links instead of following them
os.utime(path2, (long_ago_posix, long_ago_posix), follow_symlinks=False)
else:
print("Don't recognise option {0}. Try 2.utime, 2.touch, or 3.utime .".format(method))
lstat1, lstat2 = os.lstat(path1), os.lstat(path2)
print("After: {path1} atime {sa}, mtime {sm}, {path2} atime {la}, mtime {lm}".format(
path1=path1, path2=path2, sa=lstat1.st_atime, sm=lstat1.st_mtime,
la=lstat2.st_atime, lm=lstat2.st_mtime))
Here is the Python 3 os.utime()
succeeding:
% python3 ../src/utime_symlink_test.py 3.utime
Before: source atime 1514961960.0, mtime 1514961960.0, link atime 1514961960.0, mtime 1514961960.0
Desired: source unchanged, link atime 1483257600.0, mtime 1483257600.0
After: source atime 1514961960.0, mtime 1514961960.0, link atime 1483257600.0, mtime 1483257600.0
% ls -ldT source link
lrwxr-xr-x 1 myuser staff 6 1 Jan 00:00:00 2017 link -> source
drwxr-xr-x 2 myuser staff 68 2 Jan 22:46:00 2018 source
Here is the touch
call succeeding on Python 2 (tested on Mac only):
% python ../src/utime_symlink_test.py 2.touch
Before: source atime 1514961838.0, mtime 1514961838.0, link atime 1514961838.0, mtime 1514961838.0
Desired: source unchanged, link atime 1483257600.0, mtime 1483257600.0
After: source atime 1514961838.0, mtime 1514961838.0, link atime 1483257600.0, mtime 1483257600.0
% ls -ldT source link
lrwxr-xr-x 1 myuser staff 6 1 Jan 00:00:00 2017 link -> source
drwxr-xr-x 2 myuser staff 68 2 Jan 22:43:58 2018 source
Upvotes: 0
Reputation: 781726
Upgrade to Python 3.6 and use the follow_symlinks
option.
os.utime(path2, (long_ago_posix, long_ago_posix), follow_symlinks = False)
Upvotes: 3