##// END OF EJS Templates
Merge pull request #13224 from jrabinow/xdg_dir_support_posix...
Matthias Bussonnier -
r27365:6d92ccfc merge
parent child Browse files
Show More
@@ -1,205 +1,201 b''
1 1 import errno
2 2 import os
3 3 import shutil
4 4 import sys
5 5 import tempfile
6 6 import warnings
7 7 from unittest.mock import patch
8 8
9 9 from testpath import modified_env, assert_isdir, assert_isfile
10 10
11 11 from IPython import paths
12 12 from IPython.testing.decorators import skip_win32
13 13 from IPython.utils.tempdir import TemporaryDirectory
14 14
15 15 TMP_TEST_DIR = os.path.realpath(tempfile.mkdtemp())
16 16 HOME_TEST_DIR = os.path.join(TMP_TEST_DIR, "home_test_dir")
17 17 XDG_TEST_DIR = os.path.join(HOME_TEST_DIR, "xdg_test_dir")
18 18 XDG_CACHE_DIR = os.path.join(HOME_TEST_DIR, "xdg_cache_dir")
19 19 IP_TEST_DIR = os.path.join(HOME_TEST_DIR,'.ipython')
20 20
21 21 def setup_module():
22 22 """Setup testenvironment for the module:
23 23
24 24 - Adds dummy home dir tree
25 25 """
26 26 # Do not mask exceptions here. In particular, catching WindowsError is a
27 27 # problem because that exception is only defined on Windows...
28 28 os.makedirs(IP_TEST_DIR)
29 29 os.makedirs(os.path.join(XDG_TEST_DIR, 'ipython'))
30 30 os.makedirs(os.path.join(XDG_CACHE_DIR, 'ipython'))
31 31
32 32
33 33 def teardown_module():
34 34 """Teardown testenvironment for the module:
35 35
36 36 - Remove dummy home dir tree
37 37 """
38 38 # Note: we remove the parent test dir, which is the root of all test
39 39 # subdirs we may have created. Use shutil instead of os.removedirs, so
40 40 # that non-empty directories are all recursively removed.
41 41 shutil.rmtree(TMP_TEST_DIR)
42 42
43 43 def patch_get_home_dir(dirpath):
44 44 return patch.object(paths, 'get_home_dir', return_value=dirpath)
45 45
46 46
47 47 def test_get_ipython_dir_1():
48 48 """test_get_ipython_dir_1, Testcase to see if we can call get_ipython_dir without Exceptions."""
49 49 env_ipdir = os.path.join("someplace", ".ipython")
50 50 with patch.object(paths, '_writable_dir', return_value=True), \
51 51 modified_env({'IPYTHONDIR': env_ipdir}):
52 52 ipdir = paths.get_ipython_dir()
53 53
54 54 assert ipdir == env_ipdir
55 55
56 56 def test_get_ipython_dir_2():
57 57 """test_get_ipython_dir_2, Testcase to see if we can call get_ipython_dir without Exceptions."""
58 58 with patch_get_home_dir('someplace'), \
59 59 patch.object(paths, 'get_xdg_dir', return_value=None), \
60 60 patch.object(paths, '_writable_dir', return_value=True), \
61 61 patch('os.name', "posix"), \
62 62 modified_env({'IPYTHON_DIR': None,
63 63 'IPYTHONDIR': None,
64 64 'XDG_CONFIG_HOME': None
65 65 }):
66 66 ipdir = paths.get_ipython_dir()
67 67
68 68 assert ipdir == os.path.join("someplace", ".ipython")
69 69
70 70 def test_get_ipython_dir_3():
71 """test_get_ipython_dir_3, move XDG if defined, and .ipython doesn't exist."""
71 """test_get_ipython_dir_3, use XDG if defined and exists, and .ipython doesn't exist."""
72 72 tmphome = TemporaryDirectory()
73 73 try:
74 74 with patch_get_home_dir(tmphome.name), \
75 75 patch('os.name', 'posix'), \
76 76 modified_env({
77 77 'IPYTHON_DIR': None,
78 78 'IPYTHONDIR': None,
79 79 'XDG_CONFIG_HOME': XDG_TEST_DIR,
80 80 }), warnings.catch_warnings(record=True) as w:
81 81 ipdir = paths.get_ipython_dir()
82 82
83 assert ipdir == os.path.join(tmphome.name, ".ipython")
84 if sys.platform != 'darwin':
85 assert len(w) == 1
86 assert "Moving" in str(w[0])
83 assert ipdir == os.path.join(tmphome.name, XDG_TEST_DIR, "ipython")
84 assert len(w) == 0
87 85 finally:
88 86 tmphome.cleanup()
89 87
90 88 def test_get_ipython_dir_4():
91 89 """test_get_ipython_dir_4, warn if XDG and home both exist."""
92 90 with patch_get_home_dir(HOME_TEST_DIR), \
93 91 patch('os.name', 'posix'):
94 92 try:
95 93 os.mkdir(os.path.join(XDG_TEST_DIR, 'ipython'))
96 94 except OSError as e:
97 95 if e.errno != errno.EEXIST:
98 96 raise
99 97
100 98
101 99 with modified_env({
102 100 'IPYTHON_DIR': None,
103 101 'IPYTHONDIR': None,
104 102 'XDG_CONFIG_HOME': XDG_TEST_DIR,
105 103 }), warnings.catch_warnings(record=True) as w:
106 104 ipdir = paths.get_ipython_dir()
107 105
108 assert ipdir == os.path.join(HOME_TEST_DIR, ".ipython")
109 if sys.platform != 'darwin':
110 assert len(w) == 1
111 assert "Ignoring" in str(w[0])
106 assert len(w) == 1
107 assert "Ignoring" in str(w[0])
112 108
113 109
114 110 def test_get_ipython_dir_5():
115 111 """test_get_ipython_dir_5, use .ipython if exists and XDG defined, but doesn't exist."""
116 112 with patch_get_home_dir(HOME_TEST_DIR), \
117 113 patch('os.name', 'posix'):
118 114 try:
119 115 os.rmdir(os.path.join(XDG_TEST_DIR, 'ipython'))
120 116 except OSError as e:
121 117 if e.errno != errno.ENOENT:
122 118 raise
123 119
124 120 with modified_env({
125 121 'IPYTHON_DIR': None,
126 122 'IPYTHONDIR': None,
127 123 'XDG_CONFIG_HOME': XDG_TEST_DIR,
128 124 }):
129 125 ipdir = paths.get_ipython_dir()
130 126
131 127 assert ipdir == IP_TEST_DIR
132 128
133 129 def test_get_ipython_dir_6():
134 130 """test_get_ipython_dir_6, use home over XDG if defined and neither exist."""
135 131 xdg = os.path.join(HOME_TEST_DIR, 'somexdg')
136 132 os.mkdir(xdg)
137 133 shutil.rmtree(os.path.join(HOME_TEST_DIR, '.ipython'))
138 134 print(paths._writable_dir)
139 135 with patch_get_home_dir(HOME_TEST_DIR), \
140 136 patch.object(paths, 'get_xdg_dir', return_value=xdg), \
141 137 patch('os.name', 'posix'), \
142 138 modified_env({
143 139 'IPYTHON_DIR': None,
144 140 'IPYTHONDIR': None,
145 141 'XDG_CONFIG_HOME': None,
146 142 }), warnings.catch_warnings(record=True) as w:
147 143 ipdir = paths.get_ipython_dir()
148 144
149 145 assert ipdir == os.path.join(HOME_TEST_DIR, ".ipython")
150 146 assert len(w) == 0
151 147
152 148 def test_get_ipython_dir_7():
153 149 """test_get_ipython_dir_7, test home directory expansion on IPYTHONDIR"""
154 150 home_dir = os.path.normpath(os.path.expanduser('~'))
155 151 with modified_env({'IPYTHONDIR': os.path.join('~', 'somewhere')}), \
156 152 patch.object(paths, '_writable_dir', return_value=True):
157 153 ipdir = paths.get_ipython_dir()
158 154 assert ipdir == os.path.join(home_dir, "somewhere")
159 155
160 156
161 157 @skip_win32
162 158 def test_get_ipython_dir_8():
163 159 """test_get_ipython_dir_8, test / home directory"""
164 160 if not os.access("/", os.W_OK):
165 161 # test only when HOME directory actually writable
166 162 return
167 163
168 164 with patch.object(paths, "_writable_dir", lambda path: bool(path)), patch.object(
169 165 paths, "get_xdg_dir", return_value=None
170 166 ), modified_env(
171 167 {
172 168 "IPYTHON_DIR": None,
173 169 "IPYTHONDIR": None,
174 170 "HOME": "/",
175 171 }
176 172 ):
177 173 assert paths.get_ipython_dir() == "/.ipython"
178 174
179 175
180 176 def test_get_ipython_cache_dir():
181 177 with modified_env({'HOME': HOME_TEST_DIR}):
182 if os.name == 'posix' and sys.platform != 'darwin':
178 if os.name == "posix":
183 179 # test default
184 180 os.makedirs(os.path.join(HOME_TEST_DIR, ".cache"))
185 181 with modified_env({'XDG_CACHE_HOME': None}):
186 182 ipdir = paths.get_ipython_cache_dir()
187 183 assert os.path.join(HOME_TEST_DIR, ".cache", "ipython") == ipdir
188 184 assert_isdir(ipdir)
189 185
190 186 # test env override
191 187 with modified_env({"XDG_CACHE_HOME": XDG_CACHE_DIR}):
192 188 ipdir = paths.get_ipython_cache_dir()
193 189 assert_isdir(ipdir)
194 190 assert ipdir == os.path.join(XDG_CACHE_DIR, "ipython")
195 191 else:
196 192 assert paths.get_ipython_cache_dir() == paths.get_ipython_dir()
197 193
198 194 def test_get_ipython_package_dir():
199 195 ipdir = paths.get_ipython_package_dir()
200 196 assert_isdir(ipdir)
201 197
202 198
203 199 def test_get_ipython_module_path():
204 200 ipapp_path = paths.get_ipython_module_path('IPython.terminal.ipapp')
205 201 assert_isfile(ipapp_path)
@@ -1,127 +1,126 b''
1 1 """Find files and directories which IPython uses.
2 2 """
3 3 import os.path
4 4 import shutil
5 5 import tempfile
6 6 from warnings import warn
7 7
8 8 import IPython
9 9 from IPython.utils.importstring import import_item
10 10 from IPython.utils.path import (
11 11 get_home_dir,
12 12 get_xdg_dir,
13 13 get_xdg_cache_dir,
14 14 compress_user,
15 15 _writable_dir,
16 16 ensure_dir_exists,
17 17 )
18 18
19 19
20 20 def get_ipython_dir() -> str:
21 21 """Get the IPython directory for this platform and user.
22 22
23 23 This uses the logic in `get_home_dir` to find the home directory
24 24 and then adds .ipython to the end of the path.
25 25 """
26 26
27 27 env = os.environ
28 28 pjoin = os.path.join
29 29
30 30
31 31 ipdir_def = '.ipython'
32 32
33 33 home_dir = get_home_dir()
34 34 xdg_dir = get_xdg_dir()
35 35
36 36 if 'IPYTHON_DIR' in env:
37 37 warn('The environment variable IPYTHON_DIR is deprecated since IPython 3.0. '
38 38 'Please use IPYTHONDIR instead.', DeprecationWarning)
39 39 ipdir = env.get('IPYTHONDIR', env.get('IPYTHON_DIR', None))
40 40 if ipdir is None:
41 41 # not set explicitly, use ~/.ipython
42 42 ipdir = pjoin(home_dir, ipdir_def)
43 43 if xdg_dir:
44 44 # Several IPython versions (up to 1.x) defaulted to .config/ipython
45 45 # on Linux. We have decided to go back to using .ipython everywhere
46 46 xdg_ipdir = pjoin(xdg_dir, 'ipython')
47 47
48 48 if _writable_dir(xdg_ipdir):
49 49 cu = compress_user
50 50 if os.path.exists(ipdir):
51 51 warn(('Ignoring {0} in favour of {1}. Remove {0} to '
52 52 'get rid of this message').format(cu(xdg_ipdir), cu(ipdir)))
53 53 elif os.path.islink(xdg_ipdir):
54 54 warn(('{0} is deprecated. Move link to {1} to '
55 55 'get rid of this message').format(cu(xdg_ipdir), cu(ipdir)))
56 56 else:
57 warn('Moving {0} to {1}'.format(cu(xdg_ipdir), cu(ipdir)))
58 shutil.move(xdg_ipdir, ipdir)
57 ipdir = xdg_ipdir
59 58
60 59 ipdir = os.path.normpath(os.path.expanduser(ipdir))
61 60
62 61 if os.path.exists(ipdir) and not _writable_dir(ipdir):
63 62 # ipdir exists, but is not writable
64 63 warn("IPython dir '{0}' is not a writable location,"
65 64 " using a temp directory.".format(ipdir))
66 65 ipdir = tempfile.mkdtemp()
67 66 elif not os.path.exists(ipdir):
68 67 parent = os.path.dirname(ipdir)
69 68 if not _writable_dir(parent):
70 69 # ipdir does not exist and parent isn't writable
71 70 warn("IPython parent '{0}' is not a writable location,"
72 71 " using a temp directory.".format(parent))
73 72 ipdir = tempfile.mkdtemp()
74 73 else:
75 74 os.makedirs(ipdir, exist_ok=True)
76 75 assert isinstance(ipdir, str), "all path manipulation should be str(unicode), but are not."
77 76 return ipdir
78 77
79 78
80 79 def get_ipython_cache_dir() -> str:
81 80 """Get the cache directory it is created if it does not exist."""
82 81 xdgdir = get_xdg_cache_dir()
83 82 if xdgdir is None:
84 83 return get_ipython_dir()
85 84 ipdir = os.path.join(xdgdir, "ipython")
86 85 if not os.path.exists(ipdir) and _writable_dir(xdgdir):
87 86 ensure_dir_exists(ipdir)
88 87 elif not _writable_dir(xdgdir):
89 88 return get_ipython_dir()
90 89
91 90 return ipdir
92 91
93 92
94 93 def get_ipython_package_dir() -> str:
95 94 """Get the base directory where IPython itself is installed."""
96 95 ipdir = os.path.dirname(IPython.__file__)
97 96 assert isinstance(ipdir, str)
98 97 return ipdir
99 98
100 99
101 100 def get_ipython_module_path(module_str):
102 101 """Find the path to an IPython module in this version of IPython.
103 102
104 103 This will always find the version of the module that is in this importable
105 104 IPython package. This will always return the path to the ``.py``
106 105 version of the module.
107 106 """
108 107 if module_str == 'IPython':
109 108 return os.path.join(get_ipython_package_dir(), '__init__.py')
110 109 mod = import_item(module_str)
111 110 the_path = mod.__file__.replace('.pyc', '.py')
112 111 the_path = the_path.replace('.pyo', '.py')
113 112 return the_path
114 113
115 114
116 115 def locate_profile(profile='default'):
117 116 """Find the path to the folder associated with a given profile.
118 117
119 118 I.e. find $IPYTHONDIR/profile_whatever.
120 119 """
121 120 from IPython.core.profiledir import ProfileDir, ProfileDirError
122 121 try:
123 122 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), profile)
124 123 except ProfileDirError as e:
125 124 # IOError makes more sense when people are expecting a path
126 125 raise IOError("Couldn't find profile %r" % profile) from e
127 126 return pd.location
@@ -1,392 +1,392 b''
1 1 # encoding: utf-8
2 2 """
3 3 Utilities for path handling.
4 4 """
5 5
6 6 # Copyright (c) IPython Development Team.
7 7 # Distributed under the terms of the Modified BSD License.
8 8
9 9 import os
10 10 import sys
11 11 import errno
12 12 import shutil
13 13 import random
14 14 import glob
15 15 from warnings import warn
16 16
17 17 from IPython.utils.process import system
18 18 from IPython.utils.decorators import undoc
19 19
20 20 #-----------------------------------------------------------------------------
21 21 # Code
22 22 #-----------------------------------------------------------------------------
23 23 fs_encoding = sys.getfilesystemencoding()
24 24
25 25 def _writable_dir(path):
26 26 """Whether `path` is a directory, to which the user has write access."""
27 27 return os.path.isdir(path) and os.access(path, os.W_OK)
28 28
29 29 if sys.platform == 'win32':
30 30 def _get_long_path_name(path):
31 31 """Get a long path name (expand ~) on Windows using ctypes.
32 32
33 33 Examples
34 34 --------
35 35
36 36 >>> get_long_path_name('c:\\\\docume~1')
37 37 'c:\\\\Documents and Settings'
38 38
39 39 """
40 40 try:
41 41 import ctypes
42 42 except ImportError as e:
43 43 raise ImportError('you need to have ctypes installed for this to work') from e
44 44 _GetLongPathName = ctypes.windll.kernel32.GetLongPathNameW
45 45 _GetLongPathName.argtypes = [ctypes.c_wchar_p, ctypes.c_wchar_p,
46 46 ctypes.c_uint ]
47 47
48 48 buf = ctypes.create_unicode_buffer(260)
49 49 rv = _GetLongPathName(path, buf, 260)
50 50 if rv == 0 or rv > 260:
51 51 return path
52 52 else:
53 53 return buf.value
54 54 else:
55 55 def _get_long_path_name(path):
56 56 """Dummy no-op."""
57 57 return path
58 58
59 59
60 60
61 61 def get_long_path_name(path):
62 62 """Expand a path into its long form.
63 63
64 64 On Windows this expands any ~ in the paths. On other platforms, it is
65 65 a null operation.
66 66 """
67 67 return _get_long_path_name(path)
68 68
69 69
70 70 def compress_user(path):
71 71 """Reverse of :func:`os.path.expanduser`
72 72 """
73 73 home = os.path.expanduser('~')
74 74 if path.startswith(home):
75 75 path = "~" + path[len(home):]
76 76 return path
77 77
78 78 def get_py_filename(name):
79 79 """Return a valid python filename in the current directory.
80 80
81 81 If the given name is not a file, it adds '.py' and searches again.
82 82 Raises IOError with an informative message if the file isn't found.
83 83 """
84 84
85 85 name = os.path.expanduser(name)
86 86 if not os.path.isfile(name) and not name.endswith('.py'):
87 87 name += '.py'
88 88 if os.path.isfile(name):
89 89 return name
90 90 else:
91 91 raise IOError('File `%r` not found.' % name)
92 92
93 93
94 94 def filefind(filename: str, path_dirs=None) -> str:
95 95 """Find a file by looking through a sequence of paths.
96 96
97 97 This iterates through a sequence of paths looking for a file and returns
98 98 the full, absolute path of the first occurrence of the file. If no set of
99 99 path dirs is given, the filename is tested as is, after running through
100 100 :func:`expandvars` and :func:`expanduser`. Thus a simple call::
101 101
102 102 filefind('myfile.txt')
103 103
104 104 will find the file in the current working dir, but::
105 105
106 106 filefind('~/myfile.txt')
107 107
108 108 Will find the file in the users home directory. This function does not
109 109 automatically try any paths, such as the cwd or the user's home directory.
110 110
111 111 Parameters
112 112 ----------
113 113 filename : str
114 114 The filename to look for.
115 115 path_dirs : str, None or sequence of str
116 116 The sequence of paths to look for the file in. If None, the filename
117 117 need to be absolute or be in the cwd. If a string, the string is
118 118 put into a sequence and the searched. If a sequence, walk through
119 119 each element and join with ``filename``, calling :func:`expandvars`
120 120 and :func:`expanduser` before testing for existence.
121 121
122 122 Returns
123 123 -------
124 124 path : str
125 125 returns absolute path to file.
126 126
127 127 Raises
128 128 ------
129 129 IOError
130 130 """
131 131
132 132 # If paths are quoted, abspath gets confused, strip them...
133 133 filename = filename.strip('"').strip("'")
134 134 # If the input is an absolute path, just check it exists
135 135 if os.path.isabs(filename) and os.path.isfile(filename):
136 136 return filename
137 137
138 138 if path_dirs is None:
139 139 path_dirs = ("",)
140 140 elif isinstance(path_dirs, str):
141 141 path_dirs = (path_dirs,)
142 142
143 143 for path in path_dirs:
144 144 if path == '.': path = os.getcwd()
145 145 testname = expand_path(os.path.join(path, filename))
146 146 if os.path.isfile(testname):
147 147 return os.path.abspath(testname)
148 148
149 149 raise IOError("File %r does not exist in any of the search paths: %r" %
150 150 (filename, path_dirs) )
151 151
152 152
153 153 class HomeDirError(Exception):
154 154 pass
155 155
156 156
157 157 def get_home_dir(require_writable=False) -> str:
158 158 """Return the 'home' directory, as a unicode string.
159 159
160 160 Uses os.path.expanduser('~'), and checks for writability.
161 161
162 162 See stdlib docs for how this is determined.
163 163 For Python <3.8, $HOME is first priority on *ALL* platforms.
164 164 For Python >=3.8 on Windows, %HOME% is no longer considered.
165 165
166 166 Parameters
167 167 ----------
168 168 require_writable : bool [default: False]
169 169 if True:
170 170 guarantees the return value is a writable directory, otherwise
171 171 raises HomeDirError
172 172 if False:
173 173 The path is resolved, but it is not guaranteed to exist or be writable.
174 174 """
175 175
176 176 homedir = os.path.expanduser('~')
177 177 # Next line will make things work even when /home/ is a symlink to
178 178 # /usr/home as it is on FreeBSD, for example
179 179 homedir = os.path.realpath(homedir)
180 180
181 181 if not _writable_dir(homedir) and os.name == 'nt':
182 182 # expanduser failed, use the registry to get the 'My Documents' folder.
183 183 try:
184 184 import winreg as wreg
185 185 with wreg.OpenKey(
186 186 wreg.HKEY_CURRENT_USER,
187 187 r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
188 188 ) as key:
189 189 homedir = wreg.QueryValueEx(key,'Personal')[0]
190 190 except:
191 191 pass
192 192
193 193 if (not require_writable) or _writable_dir(homedir):
194 194 assert isinstance(homedir, str), "Homedir should be unicode not bytes"
195 195 return homedir
196 196 else:
197 197 raise HomeDirError('%s is not a writable dir, '
198 198 'set $HOME environment variable to override' % homedir)
199 199
200 200 def get_xdg_dir():
201 201 """Return the XDG_CONFIG_HOME, if it is defined and exists, else None.
202 202
203 203 This is only for non-OS X posix (Linux,Unix,etc.) systems.
204 204 """
205 205
206 206 env = os.environ
207 207
208 if os.name == 'posix' and sys.platform != 'darwin':
208 if os.name == "posix":
209 209 # Linux, Unix, AIX, etc.
210 210 # use ~/.config if empty OR not set
211 211 xdg = env.get("XDG_CONFIG_HOME", None) or os.path.join(get_home_dir(), '.config')
212 212 if xdg and _writable_dir(xdg):
213 213 assert isinstance(xdg, str)
214 214 return xdg
215 215
216 216 return None
217 217
218 218
219 219 def get_xdg_cache_dir():
220 220 """Return the XDG_CACHE_HOME, if it is defined and exists, else None.
221 221
222 222 This is only for non-OS X posix (Linux,Unix,etc.) systems.
223 223 """
224 224
225 225 env = os.environ
226 226
227 if os.name == 'posix' and sys.platform != 'darwin':
227 if os.name == "posix":
228 228 # Linux, Unix, AIX, etc.
229 229 # use ~/.cache if empty OR not set
230 230 xdg = env.get("XDG_CACHE_HOME", None) or os.path.join(get_home_dir(), '.cache')
231 231 if xdg and _writable_dir(xdg):
232 232 assert isinstance(xdg, str)
233 233 return xdg
234 234
235 235 return None
236 236
237 237
238 238 def expand_path(s):
239 239 """Expand $VARS and ~names in a string, like a shell
240 240
241 241 :Examples:
242 242
243 243 In [2]: os.environ['FOO']='test'
244 244
245 245 In [3]: expand_path('variable FOO is $FOO')
246 246 Out[3]: 'variable FOO is test'
247 247 """
248 248 # This is a pretty subtle hack. When expand user is given a UNC path
249 249 # on Windows (\\server\share$\%username%), os.path.expandvars, removes
250 250 # the $ to get (\\server\share\%username%). I think it considered $
251 251 # alone an empty var. But, we need the $ to remains there (it indicates
252 252 # a hidden share).
253 253 if os.name=='nt':
254 254 s = s.replace('$\\', 'IPYTHON_TEMP')
255 255 s = os.path.expandvars(os.path.expanduser(s))
256 256 if os.name=='nt':
257 257 s = s.replace('IPYTHON_TEMP', '$\\')
258 258 return s
259 259
260 260
261 261 def unescape_glob(string):
262 262 """Unescape glob pattern in `string`."""
263 263 def unescape(s):
264 264 for pattern in '*[]!?':
265 265 s = s.replace(r'\{0}'.format(pattern), pattern)
266 266 return s
267 267 return '\\'.join(map(unescape, string.split('\\\\')))
268 268
269 269
270 270 def shellglob(args):
271 271 """
272 272 Do glob expansion for each element in `args` and return a flattened list.
273 273
274 274 Unmatched glob pattern will remain as-is in the returned list.
275 275
276 276 """
277 277 expanded = []
278 278 # Do not unescape backslash in Windows as it is interpreted as
279 279 # path separator:
280 280 unescape = unescape_glob if sys.platform != 'win32' else lambda x: x
281 281 for a in args:
282 282 expanded.extend(glob.glob(a) or [unescape(a)])
283 283 return expanded
284 284
285 285
286 286 def target_outdated(target,deps):
287 287 """Determine whether a target is out of date.
288 288
289 289 target_outdated(target,deps) -> 1/0
290 290
291 291 deps: list of filenames which MUST exist.
292 292 target: single filename which may or may not exist.
293 293
294 294 If target doesn't exist or is older than any file listed in deps, return
295 295 true, otherwise return false.
296 296 """
297 297 try:
298 298 target_time = os.path.getmtime(target)
299 299 except os.error:
300 300 return 1
301 301 for dep in deps:
302 302 dep_time = os.path.getmtime(dep)
303 303 if dep_time > target_time:
304 304 #print "For target",target,"Dep failed:",dep # dbg
305 305 #print "times (dep,tar):",dep_time,target_time # dbg
306 306 return 1
307 307 return 0
308 308
309 309
310 310 def target_update(target,deps,cmd):
311 311 """Update a target with a given command given a list of dependencies.
312 312
313 313 target_update(target,deps,cmd) -> runs cmd if target is outdated.
314 314
315 315 This is just a wrapper around target_outdated() which calls the given
316 316 command if target is outdated."""
317 317
318 318 if target_outdated(target,deps):
319 319 system(cmd)
320 320
321 321
322 322 ENOLINK = 1998
323 323
324 324 def link(src, dst):
325 325 """Hard links ``src`` to ``dst``, returning 0 or errno.
326 326
327 327 Note that the special errno ``ENOLINK`` will be returned if ``os.link`` isn't
328 328 supported by the operating system.
329 329 """
330 330
331 331 if not hasattr(os, "link"):
332 332 return ENOLINK
333 333 link_errno = 0
334 334 try:
335 335 os.link(src, dst)
336 336 except OSError as e:
337 337 link_errno = e.errno
338 338 return link_errno
339 339
340 340
341 341 def link_or_copy(src, dst):
342 342 """Attempts to hardlink ``src`` to ``dst``, copying if the link fails.
343 343
344 344 Attempts to maintain the semantics of ``shutil.copy``.
345 345
346 346 Because ``os.link`` does not overwrite files, a unique temporary file
347 347 will be used if the target already exists, then that file will be moved
348 348 into place.
349 349 """
350 350
351 351 if os.path.isdir(dst):
352 352 dst = os.path.join(dst, os.path.basename(src))
353 353
354 354 link_errno = link(src, dst)
355 355 if link_errno == errno.EEXIST:
356 356 if os.stat(src).st_ino == os.stat(dst).st_ino:
357 357 # dst is already a hard link to the correct file, so we don't need
358 358 # to do anything else. If we try to link and rename the file
359 359 # anyway, we get duplicate files - see http://bugs.python.org/issue21876
360 360 return
361 361
362 362 new_dst = dst + "-temp-%04X" %(random.randint(1, 16**4), )
363 363 try:
364 364 link_or_copy(src, new_dst)
365 365 except:
366 366 try:
367 367 os.remove(new_dst)
368 368 except OSError:
369 369 pass
370 370 raise
371 371 os.rename(new_dst, dst)
372 372 elif link_errno != 0:
373 373 # Either link isn't supported, or the filesystem doesn't support
374 374 # linking, or 'src' and 'dst' are on different filesystems.
375 375 shutil.copy(src, dst)
376 376
377 377 def ensure_dir_exists(path, mode=0o755):
378 378 """ensure that a directory exists
379 379
380 380 If it doesn't exist, try to create it and protect against a race condition
381 381 if another process is doing the same.
382 382
383 383 The default permissions are 755, which differ from os.makedirs default of 777.
384 384 """
385 385 if not os.path.exists(path):
386 386 try:
387 387 os.makedirs(path, mode=mode)
388 388 except OSError as e:
389 389 if e.errno != errno.EEXIST:
390 390 raise
391 391 elif not os.path.isdir(path):
392 392 raise IOError("%r exists but is not a directory" % path)
@@ -1,504 +1,504 b''
1 1 # encoding: utf-8
2 2 """Tests for IPython.utils.path.py"""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 import os
8 8 import shutil
9 9 import sys
10 10 import tempfile
11 11 import unittest
12 12 from contextlib import contextmanager
13 13 from unittest.mock import patch
14 14 from os.path import join, abspath
15 15 from importlib import reload
16 16
17 17 import pytest
18 18
19 19 import IPython
20 20 from IPython import paths
21 21 from IPython.testing import decorators as dec
22 22 from IPython.testing.decorators import (
23 23 skip_if_not_win32,
24 24 skip_win32,
25 25 onlyif_unicode_paths,
26 26 )
27 27 from IPython.testing.tools import make_tempfile
28 28 from IPython.utils import path
29 29 from IPython.utils.tempdir import TemporaryDirectory
30 30
31 31
32 32 # Platform-dependent imports
33 33 try:
34 34 import winreg as wreg
35 35 except ImportError:
36 36 #Fake _winreg module on non-windows platforms
37 37 import types
38 38 wr_name = "winreg"
39 39 sys.modules[wr_name] = types.ModuleType(wr_name)
40 40 try:
41 41 import winreg as wreg
42 42 except ImportError:
43 43 import _winreg as wreg
44 44 #Add entries that needs to be stubbed by the testing code
45 45 (wreg.OpenKey, wreg.QueryValueEx,) = (None, None)
46 46
47 47 #-----------------------------------------------------------------------------
48 48 # Globals
49 49 #-----------------------------------------------------------------------------
50 50 env = os.environ
51 51 TMP_TEST_DIR = tempfile.mkdtemp()
52 52 HOME_TEST_DIR = join(TMP_TEST_DIR, "home_test_dir")
53 53 #
54 54 # Setup/teardown functions/decorators
55 55 #
56 56
57 57 def setup_module():
58 58 """Setup testenvironment for the module:
59 59
60 60 - Adds dummy home dir tree
61 61 """
62 62 # Do not mask exceptions here. In particular, catching WindowsError is a
63 63 # problem because that exception is only defined on Windows...
64 64 os.makedirs(os.path.join(HOME_TEST_DIR, 'ipython'))
65 65
66 66
67 67 def teardown_module():
68 68 """Teardown testenvironment for the module:
69 69
70 70 - Remove dummy home dir tree
71 71 """
72 72 # Note: we remove the parent test dir, which is the root of all test
73 73 # subdirs we may have created. Use shutil instead of os.removedirs, so
74 74 # that non-empty directories are all recursively removed.
75 75 shutil.rmtree(TMP_TEST_DIR)
76 76
77 77
78 78 def setup_environment():
79 79 """Setup testenvironment for some functions that are tested
80 80 in this module. In particular this functions stores attributes
81 81 and other things that we need to stub in some test functions.
82 82 This needs to be done on a function level and not module level because
83 83 each testfunction needs a pristine environment.
84 84 """
85 85 global oldstuff, platformstuff
86 86 oldstuff = (env.copy(), os.name, sys.platform, path.get_home_dir, IPython.__file__, os.getcwd())
87 87
88 88 def teardown_environment():
89 89 """Restore things that were remembered by the setup_environment function
90 90 """
91 91 (oldenv, os.name, sys.platform, path.get_home_dir, IPython.__file__, old_wd) = oldstuff
92 92 os.chdir(old_wd)
93 93 reload(path)
94 94
95 95 for key in list(env):
96 96 if key not in oldenv:
97 97 del env[key]
98 98 env.update(oldenv)
99 99 if hasattr(sys, 'frozen'):
100 100 del sys.frozen
101 101
102 102
103 103 # Build decorator that uses the setup_environment/setup_environment
104 104 @pytest.fixture
105 105 def environment():
106 106 setup_environment()
107 107 yield
108 108 teardown_environment()
109 109
110 110
111 111 with_environment = pytest.mark.usefixtures("environment")
112 112
113 113
114 114 @skip_if_not_win32
115 115 @with_environment
116 116 def test_get_home_dir_1():
117 117 """Testcase for py2exe logic, un-compressed lib
118 118 """
119 119 unfrozen = path.get_home_dir()
120 120 sys.frozen = True
121 121
122 122 #fake filename for IPython.__init__
123 123 IPython.__file__ = abspath(join(HOME_TEST_DIR, "Lib/IPython/__init__.py"))
124 124
125 125 home_dir = path.get_home_dir()
126 126 assert home_dir == unfrozen
127 127
128 128
129 129 @skip_if_not_win32
130 130 @with_environment
131 131 def test_get_home_dir_2():
132 132 """Testcase for py2exe logic, compressed lib
133 133 """
134 134 unfrozen = path.get_home_dir()
135 135 sys.frozen = True
136 136 #fake filename for IPython.__init__
137 137 IPython.__file__ = abspath(join(HOME_TEST_DIR, "Library.zip/IPython/__init__.py")).lower()
138 138
139 139 home_dir = path.get_home_dir(True)
140 140 assert home_dir == unfrozen
141 141
142 142
143 143 @skip_win32
144 144 @with_environment
145 145 def test_get_home_dir_3():
146 146 """get_home_dir() uses $HOME if set"""
147 147 env["HOME"] = HOME_TEST_DIR
148 148 home_dir = path.get_home_dir(True)
149 149 # get_home_dir expands symlinks
150 150 assert home_dir == os.path.realpath(env["HOME"])
151 151
152 152
153 153 @with_environment
154 154 def test_get_home_dir_4():
155 155 """get_home_dir() still works if $HOME is not set"""
156 156
157 157 if 'HOME' in env: del env['HOME']
158 158 # this should still succeed, but we don't care what the answer is
159 159 home = path.get_home_dir(False)
160 160
161 161 @skip_win32
162 162 @with_environment
163 163 def test_get_home_dir_5():
164 164 """raise HomeDirError if $HOME is specified, but not a writable dir"""
165 165 env['HOME'] = abspath(HOME_TEST_DIR+'garbage')
166 166 # set os.name = posix, to prevent My Documents fallback on Windows
167 167 os.name = 'posix'
168 168 pytest.raises(path.HomeDirError, path.get_home_dir, True)
169 169
170 170 # Should we stub wreg fully so we can run the test on all platforms?
171 171 @skip_if_not_win32
172 172 @with_environment
173 173 def test_get_home_dir_8():
174 174 """Using registry hack for 'My Documents', os=='nt'
175 175
176 176 HOMESHARE, HOMEDRIVE, HOMEPATH, USERPROFILE and others are missing.
177 177 """
178 178 os.name = 'nt'
179 179 # Remove from stub environment all keys that may be set
180 180 for key in ['HOME', 'HOMESHARE', 'HOMEDRIVE', 'HOMEPATH', 'USERPROFILE']:
181 181 env.pop(key, None)
182 182
183 183 class key:
184 184 def __enter__(self):
185 185 pass
186 186 def Close(self):
187 187 pass
188 188 def __exit__(*args, **kwargs):
189 189 pass
190 190
191 191 with patch.object(wreg, 'OpenKey', return_value=key()), \
192 192 patch.object(wreg, 'QueryValueEx', return_value=[abspath(HOME_TEST_DIR)]):
193 193 home_dir = path.get_home_dir()
194 194 assert home_dir == abspath(HOME_TEST_DIR)
195 195
196 196 @with_environment
197 197 def test_get_xdg_dir_0():
198 198 """test_get_xdg_dir_0, check xdg_dir"""
199 199 reload(path)
200 200 path._writable_dir = lambda path: True
201 201 path.get_home_dir = lambda : 'somewhere'
202 202 os.name = "posix"
203 203 sys.platform = "linux2"
204 204 env.pop('IPYTHON_DIR', None)
205 205 env.pop('IPYTHONDIR', None)
206 206 env.pop('XDG_CONFIG_HOME', None)
207 207
208 208 assert path.get_xdg_dir() == os.path.join("somewhere", ".config")
209 209
210 210
211 211 @with_environment
212 212 def test_get_xdg_dir_1():
213 213 """test_get_xdg_dir_1, check nonexistent xdg_dir"""
214 214 reload(path)
215 215 path.get_home_dir = lambda : HOME_TEST_DIR
216 216 os.name = "posix"
217 217 sys.platform = "linux2"
218 218 env.pop('IPYTHON_DIR', None)
219 219 env.pop('IPYTHONDIR', None)
220 220 env.pop('XDG_CONFIG_HOME', None)
221 221 assert path.get_xdg_dir() is None
222 222
223 223 @with_environment
224 224 def test_get_xdg_dir_2():
225 225 """test_get_xdg_dir_2, check xdg_dir default to ~/.config"""
226 226 reload(path)
227 227 path.get_home_dir = lambda : HOME_TEST_DIR
228 228 os.name = "posix"
229 229 sys.platform = "linux2"
230 230 env.pop('IPYTHON_DIR', None)
231 231 env.pop('IPYTHONDIR', None)
232 232 env.pop('XDG_CONFIG_HOME', None)
233 233 cfgdir=os.path.join(path.get_home_dir(), '.config')
234 234 if not os.path.exists(cfgdir):
235 235 os.makedirs(cfgdir)
236 236
237 237 assert path.get_xdg_dir() == cfgdir
238 238
239 239 @with_environment
240 240 def test_get_xdg_dir_3():
241 """test_get_xdg_dir_3, check xdg_dir not used on OS X"""
241 """test_get_xdg_dir_3, check xdg_dir not used on non-posix systems"""
242 242 reload(path)
243 243 path.get_home_dir = lambda : HOME_TEST_DIR
244 os.name = "posix"
245 sys.platform = "darwin"
244 os.name = "nt"
245 sys.platform = "win32"
246 246 env.pop('IPYTHON_DIR', None)
247 247 env.pop('IPYTHONDIR', None)
248 248 env.pop('XDG_CONFIG_HOME', None)
249 249 cfgdir=os.path.join(path.get_home_dir(), '.config')
250 250 os.makedirs(cfgdir, exist_ok=True)
251 251
252 252 assert path.get_xdg_dir() is None
253 253
254 254 def test_filefind():
255 255 """Various tests for filefind"""
256 256 f = tempfile.NamedTemporaryFile()
257 257 # print 'fname:',f.name
258 258 alt_dirs = paths.get_ipython_dir()
259 259 t = path.filefind(f.name, alt_dirs)
260 260 # print 'found:',t
261 261
262 262
263 263 @dec.skip_if_not_win32
264 264 def test_get_long_path_name_win32():
265 265 with TemporaryDirectory() as tmpdir:
266 266
267 267 # Make a long path. Expands the path of tmpdir prematurely as it may already have a long
268 268 # path component, so ensure we include the long form of it
269 269 long_path = os.path.join(path.get_long_path_name(tmpdir), 'this is my long path name')
270 270 os.makedirs(long_path)
271 271
272 272 # Test to see if the short path evaluates correctly.
273 273 short_path = os.path.join(tmpdir, 'THISIS~1')
274 274 evaluated_path = path.get_long_path_name(short_path)
275 275 assert evaluated_path.lower() == long_path.lower()
276 276
277 277
278 278 @dec.skip_win32
279 279 def test_get_long_path_name():
280 280 p = path.get_long_path_name("/usr/local")
281 281 assert p == "/usr/local"
282 282
283 283
284 284 class TestRaiseDeprecation(unittest.TestCase):
285 285
286 286 @dec.skip_win32 # can't create not-user-writable dir on win
287 287 @with_environment
288 288 def test_not_writable_ipdir(self):
289 289 tmpdir = tempfile.mkdtemp()
290 290 os.name = "posix"
291 291 env.pop('IPYTHON_DIR', None)
292 292 env.pop('IPYTHONDIR', None)
293 293 env.pop('XDG_CONFIG_HOME', None)
294 294 env['HOME'] = tmpdir
295 295 ipdir = os.path.join(tmpdir, '.ipython')
296 296 os.mkdir(ipdir, 0o555)
297 297 try:
298 298 open(os.path.join(ipdir, "_foo_"), 'w').close()
299 299 except IOError:
300 300 pass
301 301 else:
302 302 # I can still write to an unwritable dir,
303 303 # assume I'm root and skip the test
304 304 pytest.skip("I can't create directories that I can't write to")
305 305
306 306 with self.assertWarnsRegex(UserWarning, 'is not a writable location'):
307 307 ipdir = paths.get_ipython_dir()
308 308 env.pop('IPYTHON_DIR', None)
309 309
310 310 @with_environment
311 311 def test_get_py_filename():
312 312 os.chdir(TMP_TEST_DIR)
313 313 with make_tempfile("foo.py"):
314 314 assert path.get_py_filename("foo.py") == "foo.py"
315 315 assert path.get_py_filename("foo") == "foo.py"
316 316 with make_tempfile("foo"):
317 317 assert path.get_py_filename("foo") == "foo"
318 318 pytest.raises(IOError, path.get_py_filename, "foo.py")
319 319 pytest.raises(IOError, path.get_py_filename, "foo")
320 320 pytest.raises(IOError, path.get_py_filename, "foo.py")
321 321 true_fn = "foo with spaces.py"
322 322 with make_tempfile(true_fn):
323 323 assert path.get_py_filename("foo with spaces") == true_fn
324 324 assert path.get_py_filename("foo with spaces.py") == true_fn
325 325 pytest.raises(IOError, path.get_py_filename, '"foo with spaces.py"')
326 326 pytest.raises(IOError, path.get_py_filename, "'foo with spaces.py'")
327 327
328 328 @onlyif_unicode_paths
329 329 def test_unicode_in_filename():
330 330 """When a file doesn't exist, the exception raised should be safe to call
331 331 str() on - i.e. in Python 2 it must only have ASCII characters.
332 332
333 333 https://github.com/ipython/ipython/issues/875
334 334 """
335 335 try:
336 336 # these calls should not throw unicode encode exceptions
337 337 path.get_py_filename('fooéè.py')
338 338 except IOError as ex:
339 339 str(ex)
340 340
341 341
342 342 class TestShellGlob(unittest.TestCase):
343 343
344 344 @classmethod
345 345 def setUpClass(cls):
346 346 cls.filenames_start_with_a = ['a0', 'a1', 'a2']
347 347 cls.filenames_end_with_b = ['0b', '1b', '2b']
348 348 cls.filenames = cls.filenames_start_with_a + cls.filenames_end_with_b
349 349 cls.tempdir = TemporaryDirectory()
350 350 td = cls.tempdir.name
351 351
352 352 with cls.in_tempdir():
353 353 # Create empty files
354 354 for fname in cls.filenames:
355 355 open(os.path.join(td, fname), 'w').close()
356 356
357 357 @classmethod
358 358 def tearDownClass(cls):
359 359 cls.tempdir.cleanup()
360 360
361 361 @classmethod
362 362 @contextmanager
363 363 def in_tempdir(cls):
364 364 save = os.getcwd()
365 365 try:
366 366 os.chdir(cls.tempdir.name)
367 367 yield
368 368 finally:
369 369 os.chdir(save)
370 370
371 371 def check_match(self, patterns, matches):
372 372 with self.in_tempdir():
373 373 # glob returns unordered list. that's why sorted is required.
374 374 assert sorted(path.shellglob(patterns)) == sorted(matches)
375 375
376 376 def common_cases(self):
377 377 return [
378 378 (['*'], self.filenames),
379 379 (['a*'], self.filenames_start_with_a),
380 380 (['*c'], ['*c']),
381 381 (['*', 'a*', '*b', '*c'], self.filenames
382 382 + self.filenames_start_with_a
383 383 + self.filenames_end_with_b
384 384 + ['*c']),
385 385 (['a[012]'], self.filenames_start_with_a),
386 386 ]
387 387
388 388 @skip_win32
389 389 def test_match_posix(self):
390 390 for (patterns, matches) in self.common_cases() + [
391 391 ([r'\*'], ['*']),
392 392 ([r'a\*', 'a*'], ['a*'] + self.filenames_start_with_a),
393 393 ([r'a\[012]'], ['a[012]']),
394 394 ]:
395 395 self.check_match(patterns, matches)
396 396
397 397 @skip_if_not_win32
398 398 def test_match_windows(self):
399 399 for (patterns, matches) in self.common_cases() + [
400 400 # In windows, backslash is interpreted as path
401 401 # separator. Therefore, you can't escape glob
402 402 # using it.
403 403 ([r'a\*', 'a*'], [r'a\*'] + self.filenames_start_with_a),
404 404 ([r'a\[012]'], [r'a\[012]']),
405 405 ]:
406 406 self.check_match(patterns, matches)
407 407
408 408
409 409 # TODO : pytest.mark.parametrise once nose is gone.
410 410 def test_unescape_glob():
411 411 assert path.unescape_glob(r"\*\[\!\]\?") == "*[!]?"
412 412 assert path.unescape_glob(r"\\*") == r"\*"
413 413 assert path.unescape_glob(r"\\\*") == r"\*"
414 414 assert path.unescape_glob(r"\\a") == r"\a"
415 415 assert path.unescape_glob(r"\a") == r"\a"
416 416
417 417
418 418 @onlyif_unicode_paths
419 419 def test_ensure_dir_exists():
420 420 with TemporaryDirectory() as td:
421 421 d = os.path.join(td, 'βˆ‚ir')
422 422 path.ensure_dir_exists(d) # create it
423 423 assert os.path.isdir(d)
424 424 path.ensure_dir_exists(d) # no-op
425 425 f = os.path.join(td, 'Ζ’ile')
426 426 open(f, 'w').close() # touch
427 427 with pytest.raises(IOError):
428 428 path.ensure_dir_exists(f)
429 429
430 430 class TestLinkOrCopy(unittest.TestCase):
431 431 def setUp(self):
432 432 self.tempdir = TemporaryDirectory()
433 433 self.src = self.dst("src")
434 434 with open(self.src, "w") as f:
435 435 f.write("Hello, world!")
436 436
437 437 def tearDown(self):
438 438 self.tempdir.cleanup()
439 439
440 440 def dst(self, *args):
441 441 return os.path.join(self.tempdir.name, *args)
442 442
443 443 def assert_inode_not_equal(self, a, b):
444 444 assert (
445 445 os.stat(a).st_ino != os.stat(b).st_ino
446 446 ), "%r and %r do reference the same indoes" % (a, b)
447 447
448 448 def assert_inode_equal(self, a, b):
449 449 assert (
450 450 os.stat(a).st_ino == os.stat(b).st_ino
451 451 ), "%r and %r do not reference the same indoes" % (a, b)
452 452
453 453 def assert_content_equal(self, a, b):
454 454 with open(a) as a_f:
455 455 with open(b) as b_f:
456 456 assert a_f.read() == b_f.read()
457 457
458 458 @skip_win32
459 459 def test_link_successful(self):
460 460 dst = self.dst("target")
461 461 path.link_or_copy(self.src, dst)
462 462 self.assert_inode_equal(self.src, dst)
463 463
464 464 @skip_win32
465 465 def test_link_into_dir(self):
466 466 dst = self.dst("some_dir")
467 467 os.mkdir(dst)
468 468 path.link_or_copy(self.src, dst)
469 469 expected_dst = self.dst("some_dir", os.path.basename(self.src))
470 470 self.assert_inode_equal(self.src, expected_dst)
471 471
472 472 @skip_win32
473 473 def test_target_exists(self):
474 474 dst = self.dst("target")
475 475 open(dst, "w").close()
476 476 path.link_or_copy(self.src, dst)
477 477 self.assert_inode_equal(self.src, dst)
478 478
479 479 @skip_win32
480 480 def test_no_link(self):
481 481 real_link = os.link
482 482 try:
483 483 del os.link
484 484 dst = self.dst("target")
485 485 path.link_or_copy(self.src, dst)
486 486 self.assert_content_equal(self.src, dst)
487 487 self.assert_inode_not_equal(self.src, dst)
488 488 finally:
489 489 os.link = real_link
490 490
491 491 @skip_if_not_win32
492 492 def test_windows(self):
493 493 dst = self.dst("target")
494 494 path.link_or_copy(self.src, dst)
495 495 self.assert_content_equal(self.src, dst)
496 496
497 497 def test_link_twice(self):
498 498 # Linking the same file twice shouldn't leave duplicates around.
499 499 # See https://github.com/ipython/ipython/issues/6450
500 500 dst = self.dst('target')
501 501 path.link_or_copy(self.src, dst)
502 502 path.link_or_copy(self.src, dst)
503 503 self.assert_inode_equal(self.src, dst)
504 504 assert sorted(os.listdir(self.tempdir.name)) == ["src", "target"]
General Comments 0
You need to be logged in to leave comments. Login now