1import os
2import stat
3from os.path import abspath, split, join, isabs
4
5
6def realpath(filename, *, strict=False):
7 """Return the canonical path of the specified filename, eliminating any
8symbolic links encountered in the path."""
9 filename = os.fspath(filename)
10 path, ok = _joinrealpath(filename[:0], filename, strict, {})
11 return abspath(path)
12
13
14# Join two paths, normalizing and eliminating any symbolic links
15# encountered in the second path.
16def _joinrealpath(path, rest, strict, seen):
17 if isinstance(path, bytes):
18 sep = b'/'
19 curdir = b'.'
20 pardir = b'..'
21 else:
22 sep = '/'
23 curdir = '.'
24 pardir = '..'
25
26 if isabs(rest):
27 rest = rest[1:]
28 path = sep
29
30 while rest:
31 name, _, rest = rest.partition(sep)
32 if not name or name == curdir:
33 # current dir
34 continue
35 if name == pardir:
36 # parent dir
37 if path:
38 path, name = split(path)
39 if name == pardir:
40 path = join(path, pardir, pardir)
41 else:
42 path = pardir
43 continue
44 newpath = join(path, name)
45 try:
46 st = os.lstat(newpath)
47 except OSError:
48 if strict:
49 raise
50 is_link = False
51 else:
52 is_link = stat.S_ISLNK(st.st_mode)
53 if not is_link:
54 path = newpath
55 continue
56 # Resolve the symbolic link
57 if newpath in seen:
58 # Already seen this path
59 path = seen[newpath]
60 if path is not None:
61 # use cached value
62 continue
63 # The symlink is not resolved, so we must have a symlink loop.
64 if strict:
65 # Raise OSError(errno.ELOOP)
66 os.stat(newpath)
67 else:
68 # Return already resolved part + rest of the path unchanged.
69 return join(newpath, rest), False
70 seen[newpath] = None # not resolved symlink
71 path, ok = _joinrealpath(path, os.readlink(newpath), strict, seen)
72 if not ok:
73 return join(path, rest), False
74 seen[newpath] = path # resolved symlink
75
76 return path, True