Modifying a symlink in python

PythonSymlink

Python Problem Overview


How do I change a symlink to point from one file to another in Python?

The os.symlink function only seems to work to create new symlinks.

Python Solutions


Solution 1 - Python

If you need an atomic modification, unlinking won't work.

A better solution would be to create a new temporary symlink, and then rename it over the existing one:

os.symlink(target, tmpLink)
os.rename(tmpLink, linkName)

You can check to make sure it was updated correctly too:

if os.path.realpath(linkName) == target:
    # Symlink was updated

According to the documentation for os.rename though, there may be no way to atomically change a symlink in Windows. In that case, you would just delete and re-create.

Solution 2 - Python

A little function for Python2 which tries to symlink and if it fails because of an existing file, it removes it and links again. Check [Tom Hale's answer] for a up-to-date solution.

import os, errno

def symlink_force(target, link_name):
    try:
        os.symlink(target, link_name)
    except OSError, e:
        if e.errno == errno.EEXIST:
            os.remove(link_name)
            os.symlink(target, link_name)
        else:
            raise e

For python3 except condition should be except OSError as e:
[Tom Hale's answer]: https://stackoverflow.com/a/55742015/825924

Solution 3 - Python

You could os.unlink() it first, and then re-create using os.symlink() to point to the new target.

Solution 4 - Python

Given overwrite=True, this function will safely overwrite an existing file with a symlink.

It is cognisant of race conditions, which is why it is not short, but it is safe.

import os, tempfile

def symlink(target, link_name, overwrite=False):
    '''
    Create a symbolic link named link_name pointing to target.
    If link_name exists then FileExistsError is raised, unless overwrite=True.
    When trying to overwrite a directory, IsADirectoryError is raised.
    '''

    if not overwrite:
        os.symlink(target, link_name)
        return

    # os.replace() may fail if files are on different filesystems
    link_dir = os.path.dirname(link_name)

    # Create link to target with temporary filename
    while True:
        temp_link_name = tempfile.mktemp(dir=link_dir)

        # os.* functions mimic as closely as possible system functions
        # The POSIX symlink() returns EEXIST if link_name already exists
        # https://pubs.opengroup.org/onlinepubs/9699919799/functions/symlink.html
        try:
            os.symlink(target, temp_link_name)
            break
        except FileExistsError:
            pass

    # Replace link_name with temp_link_name
    try:
        # Pre-empt os.replace on a directory with a nicer message
        if not os.path.islink(link_name) and os.path.isdir(link_name):
            raise IsADirectoryError(f"Cannot symlink over existing directory: '{link_name}'")
        os.replace(temp_link_name, link_name)
    except:
        if os.path.islink(temp_link_name):
            os.remove(temp_link_name)
        raise

Notes for pedants:

  1. If the function fails (e.g. computer crashes), an additional random link to the target might exist.

  2. An unlikely race condition still remains: the symlink created at the randomly-named temp_link_name could be modified by another process before replacing link_name.

I raised a python issue to highlight the issues of os.symlink() requiring the target to not exist, where I was advised to raise my suggestion on the python-ideas mailing list

Credit to Robert Siemer’s input.

Solution 5 - Python

I researched this question recently, and found out that the best way is indeed to unlink and then symlink. But if you need just to fix broken links, for example with auto-replace, then you can do os.readlink:

for f in os.listdir(dir):
    path = os.path.join(dir, f)
    old_link = os.readlink(path)
    new_link = old_link.replace(before, after)
    os.unlink(path)
    os.symlink(new_link, path)

Solution 6 - Python

Don't forget to add a raise command in the case when e.errno != errno.EEXIST You don't want to hide an error then:

if e.errno == errno.EEXIST:
     os.remove(link_name)
     os.symlink(target, link_name)
else:
    raise

Solution 7 - Python

A quick and easy solution:

while True:
     try:
         os.symlink(target, link_name)
         break
     except FileExistsError:
         os.remove(link_name)

However this has a race condition when replacing a symlink which should always exist, eg:

 /lib/critical.so -> /lib/critical.so.1.2

When upgrading by:

 my_symlink('/lib/critical.so.2.0', '/lib/critical.so')

There is a point in time when /lib/critical.so doesn't exist.

This answer avoids the race condition.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionmeteoritepanamaView Question on Stackoverflow
Solution 1 - PythonlellimecnarView Answer on Stackoverflow
Solution 2 - PythonRobert SiemerView Answer on Stackoverflow
Solution 3 - PythonNPEView Answer on Stackoverflow
Solution 4 - PythonTom HaleView Answer on Stackoverflow
Solution 5 - PythonculebrónView Answer on Stackoverflow
Solution 6 - PythoncriveraView Answer on Stackoverflow
Solution 7 - PythonTom HaleView Answer on Stackoverflow