File: mergeall-products/unzipped/docetc/miscnotes/demo-3.0-symlinks-unix.txt
------------------------------------------------------------------------------------
Demo Unix symlinks on Mac OS X: py2app Mac app bundles are full of them.
Handling symlinks properly requires algorithmic changes:
-mergeall: copy, don't follow (else multiple copies of linked data!)
-cpall: ditto for items nested in a tree being copied as a whole
This assumes links are relative, not absolute (else not transportable).
See demo-3.0-windows-symlinks.txt for a demo of the same behavior on Windows.
------------------------------------------------------------------------------------
# The test folder
/.../PyEdit.app/Contents/Frameworks/Python.framework$ ls -l
total 16
lrwxr-xr-x 1 blue staff 23 Jan 23 20:36 Python -> Versions/Current/Python
lrwxr-xr-x 1 blue staff 26 Jan 23 20:36 Resources -> Versions/Current/Resources
drwxr-xr-x 4 blue staff 136 Jan 23 20:36 Versions
/.../PyEdit.app/Contents/Frameworks/Python.framework$ ls -l Versions/Current
lrwxr-xr-x 1 blue staff 3 Jan 23 20:36 Versions/Current -> 3.5
/.../PyEdit.app/Contents/Frameworks/Python.framework$ ls -l Versions/Current/
total 9856
-rwxr-xr-x 1 blue staff 5043488 Jan 23 20:36 Python
drwxr-xr-x 3 blue staff 102 Jan 23 20:36 Resources
drwxr-xr-x 3 blue staff 102 Jan 23 20:36 include
drwxr-xr-x 3 blue staff 102 Jan 23 20:36 lib
/.../PyEdit.app/Contents/Frameworks/Python.framework$ ls -l Versions/3.5
total 9856
-rwxr-xr-x 1 blue staff 5043488 Jan 23 20:36 Python
drwxr-xr-x 3 blue staff 102 Jan 23 20:36 Resources
drwxr-xr-x 3 blue staff 102 Jan 23 20:36 include
drwxr-xr-x 3 blue staff 102 Jan 23 20:36 lib
-----------------------------------------------------------------------------------------
# Identifying links with os.path.*(): isfile+islink (file), isdir+islink (dir)
/.../PyEdit.app/Contents/Frameworks/Python.framework$ py3
>>> import os
>>> os.path.isfile('Python')
True
>>> os.path.isdir('Python')
False
>>> os.path.islink('Python')
True
>>> os.path.isfile('Versions/Current/Python')
True
>>> os.path.isfile('Resources')
False
>>> os.path.isdir('Resources')
True
>>> os.path.islink('Resources')
True
>>> os.readlink('Resources')
'Versions/Current/Resources'
>>> os.path.isdir('Versions/Current/Resources')
True
>>> os.path.islink('Versions/Current'), os.path.isdir('Versions/Current')
(True, True)
>>> os.readlink('Versions/Current')
'3.5'
>>> os.listdir('Versions/Current')
['include', 'lib', 'Python', 'Resources']
>>> os.listdir('Versions/3.5')
['include', 'lib', 'Python', 'Resources']
-----------------------------------------------------------------------------------------
# Alternative: lstat() [or stat(follow_symlinks=False in 3.3+]
# Neither returns True for file or dir if it's a symlink
# follow_symlinks is available in py 3.3+ only, where lstat is equivalent to
# os.stat(path, follow_symlinks=False), but lstat() is available in earlier pys;
# lstat() is an alias for stat() on platforms without symlinks;
# stat: item referenced (only - != os.path)
>>> for item in ('Resources', 'Python', 'Versions', 'pyconfig.h'):
... s = os.stat(item)
... print('%-10s' % item, stat.S_ISREG(s.st_mode), stat.S_ISDIR(s.st_mode), stat.S_ISLNK(s.st_mode))
...
Resources False True False
Python True False False
Versions False True False
pyconfig.h True False False
# lstat: link itself (in all Py)
>>> for item in ('Resources', 'Python', 'Versions', 'pyconfig.h'):
... s = os.lstat(item)
... print('%-10s' % item, stat.S_ISREG(s.st_mode), stat.S_ISDIR(s.st_mode), stat.S_ISLNK(s.st_mode))
...
Resources False False True
Python False False True
Versions False True False
pyconfig.h True False False
# stat(follow): link itself (where available)
>>> for item in ('Resources', 'Python', 'Versions', 'pyconfig.h'):
... s = os.stat(item, follow_symlinks=False)
... print('%-10s' % item, stat.S_ISREG(s.st_mode), stat.S_ISDIR(s.st_mode), stat.S_ISLNK(s.st_mode))
...
Resources False False True
Python False False True
Versions False True False
pyconfig.h True False False
-----------------------------------------------------------------------------------------
# Alternative: os.scandir()/DirEntry objects, available in py 3.5+ only
# With follow_symlinks=False, doesn't return True for file or dir if it's a symlink
# Without follow_symlinks, same as os.path.*()
>>> os.listdir('.')
['newfifo', 'newlink1', 'newlink2', 'newlink3', 'pyconfig.h', 'Python', 'Resources', 'Versions']
# same as os.path
>>> ds = os.scandir('.')
>>> for d in ds:
... if d.name[0] != 'n':
... print('%-10s' % d.name,
... d.is_file(), d.is_dir(), d.is_symlink())
...
pyconfig.h True False False
Python True False True
Resources False True True
Versions False True False
# same as os.lstat
>>> ds = os.scandir('.')
>>> for d in ds:
... if d.name[0] != 'n':
... print('%-10s' % d.name,
... d.is_file(follow_symlinks=False), d.is_dir(follow_symlinks=False), d.is_symlink())
...
pyconfig.h True False False
Python False False True
Resources False False True
Versions False True False
-----------------------------------------------------------------------------------------
# Copying links - same whether symlink links to file or dir
>>> os.symlink(os.readlink('Python'), 'newlink1')
>>> os.path.isfile('newlink1')
True
>>> os.path.isdir('newlink1')
False
>>> os.path.islink('newlink1')
True
>>> os.readlink('newlink1')
'Versions/Current/Python'
>>> os.symlink(os.readlink('Resources'), 'newlink2')
>>> os.path.isfile('newlink2')
False
>>> os.path.isdir('newlink2')
True
>>> os.path.islink('newlink2')
True
>>> os.readlink('newlink2')
'Versions/Current/Resources'
-----------------------------------------------------------------------------------------
# The folder contents with new links
/.../PyEdit.app/Contents/Frameworks/Python.framework$ ls -l
total 32
lrwxr-xr-x 1 blue staff 23 Jan 23 20:36 Python -> Versions/Current/Python
lrwxr-xr-x 1 blue staff 26 Jan 23 20:36 Resources -> Versions/Current/Resources
drwxr-xr-x 4 blue staff 136 Jan 23 20:36 Versions
prw-r--r-- 1 blue staff 0 Jan 25 14:10 newfifo
lrwxr-xr-x 1 blue staff 23 Jan 25 14:13 newlink1 -> Versions/Current/Python
lrwxr-xr-x 1 blue staff 26 Jan 25 14:14 newlink2 -> Versions/Current/Resources
-----------------------------------------------------------------------------------------
# Why you may care:
# isfile() links will open the referenced file, not link (redundant copies?)
>>> os.path.isfile('Python')
True
>>> os.path.islink('Python')
True
>>> f = open('Python', 'rb')
>>> b = f.read()
>>> len(b)
5043488
>>>
>>> b[:70]
b'\xca\xfe\xba\xbe\x00\x00\x00\x02\x00\x00\x00\x07\x00\x00\x00\x03\x00\x00\x10\x00\x00#\xfc\xd4\x00...
>>> b[-70:]
b'\x00_wcsxfrm\x00_wmemcmp\x00_write\x00_writev\x00dyld_stub_binder\x00radr://5614542\x00\x00\x00\x00\x00'
>>>
>>> os.readlink('Python')
'Versions/Current/Python'
>>> os.path.getsize('Versions/Current/Python')
5043488
-----------------------------------------------------------------------------------------
# Why you may care:
# isdir() links will yield listings of the referenced dir (redunant walks and copies?)
>>> os.path.isdir('Resources')
True
>>> os.path.islink('Resources')
True
>>> os.listdir('Resources')
['Info.plist']
>>> os.readlink('Resources')
'Versions/Current/Resources'
>>> os.listdir('Versions/Current/Resources')
['Info.plist']
-----------------------------------------------------------------------------------------
# Ditto for links to links
>>> os.path.islink('Versions/Current'), os.path.isdir('Versions/Current')
(True, True)
>>> os.readlink('Versions/Current')
'3.5'
>>> os.listdir('Versions/Current')
['include', 'lib', 'Python', 'Resources']
>>> os.listdir('Versions/3.5')
['include', 'lib', 'Python', 'Resources']
-----------------------------------------------------------------------------------------
# Ditto for newly-created links - this is what the copies will look like
>>> os.path.getsize('newlink1')
5043488
>>> os.listdir('newlink2')
['Info.plist']
-----------------------------------------------------------------------------------------
# And fifos are not file, dir, or link in any (plus others? - mount points,... pass)
>>> os.mkfifo('newfifo')
>>> os.path.isfile('newfifo')
False
>>> os.path.isdir('newfifo')
False
>>> os.path.islink('newfifo')
False
>>> s = os.lstat('newfifo')
>>> print(stat.S_ISREG(s.st_mode), stat.S_ISDIR(s.st_mode), stat.S_ISLNK(s.st_mode))
False False False
>>> ds = os.scandir('.')
>>> for d in ds:
... print('%-10s' % d.name, d.is_file(follow_symlinks=False), d.is_dir(follow_symlinks=False), d.is_symlink())
...
newfifo False False False
etc...
-----------------------------------------------------------------------------------------
# BUT symbolic links are not portable between Windows and Unix (if "/" or "\" in paths)
# See demo-3.0-windows-symlinks.txt for the Windows side of this story ("/" fails there)
>>> os.listdir('..')
['Python.framework', 'Tcl.framework', 'Tk.framework']
# Folder link
>>> os.symlink('../Tk.framework', 'uplink') # Unix -> Unix okay
>>> os.readlink('uplink')
'../Tk.framework'
>>>
>>> os.listdir('uplink')
['libtkstub8.5.a', 'PrivateHeaders', 'Resources', 'Tk', 'tkConfig.sh', 'Versions']
>>> os.symlink('../Tk.framework', 'uplink')
FileExistsError: [Errno 17] File exists: '../Tk.framework' -> 'uplink'
>>>
>>> os.remove('uplink')
>>> os.symlink(r'..\Tk.framework', 'uplink') # Windows -> Unix fails
>>> os.readlink('uplink')
'..\\Tk.framework'
>>>
>>> os.listdir('uplink')
FileNotFoundError: [Errno 2] No such file or directory: 'uplink'
>>>
>>> os.open('uplink')
TypeError: Required argument 'flags' (pos 2) not found
# File link
>>> open('../temp.txt', 'w').write('spam\n')
5
>>> os.remove('uplink')
>>> os.symlink(r'../temp.txt', 'uplink') # Unix -> Unix okay
>>> os.readlink('uplink')
'../temp.txt'
>>> open('uplink').read()
'spam\n'
>>>
>>> os.remove('uplink')
>>> os.symlink(r'..\temp.txt', 'uplink') # Windows -> Unix fails
>>> os.readlink('uplink')
'..\\temp.txt'
>>> open('uplink').read()
FileNotFoundError: [Errno 2] No such file or directory: 'uplink'