Fix crashes with invisible Outliners on fullscreen or window closing
[blender.git] / release / scripts / modules / bpy / path.py
1 # ##### BEGIN GPL LICENSE BLOCK #####
2 #
3 #  This program is free software; you can redistribute it and/or
4 #  modify it under the terms of the GNU General Public License
5 #  as published by the Free Software Foundation; either version 2
6 #  of the License, or (at your option) any later version.
7 #
8 #  This program is distributed in the hope that it will be useful,
9 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 #  GNU General Public License for more details.
12 #
13 #  You should have received a copy of the GNU General Public License
14 #  along with this program; if not, write to the Free Software Foundation,
15 #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 #
17 # ##### END GPL LICENSE BLOCK #####
18
19 # <pep8-80 compliant>
20
21 """
22 This module has a similar scope to os.path, containing utility
23 functions for dealing with paths in Blender.
24 """
25
26 __all__ = (
27     "abspath",
28     "basename",
29     "clean_name",
30     "display_name",
31     "display_name_to_filepath",
32     "display_name_from_filepath",
33     "ensure_ext",
34     "extensions_image",
35     "extensions_movie",
36     "extensions_audio",
37     "is_subdir",
38     "module_names",
39     "native_pathsep",
40     "reduce_dirs",
41     "relpath",
42     "resolve_ncase",
43 )
44
45 import bpy as _bpy
46 import os as _os
47
48 from _bpy_path import (
49     extensions_audio,
50     extensions_movie,
51     extensions_image,
52 )
53
54
55 def _getattr_bytes(var, attr):
56     return var.path_resolve(attr, False).as_bytes()
57
58
59 def abspath(path, start=None, library=None):
60     """
61     Returns the absolute path relative to the current blend file
62     using the "//" prefix.
63
64     :arg start: Relative to this path,
65        when not set the current filename is used.
66     :type start: string or bytes
67     :arg library: The library this path is from. This is only included for
68        convenience, when the library is not None its path replaces *start*.
69     :type library: :class:`bpy.types.Library`
70     """
71     if isinstance(path, bytes):
72         if path.startswith(b"//"):
73             if library:
74                 start = _os.path.dirname(
75                     abspath(_getattr_bytes(library, "filepath")))
76             return _os.path.join(
77                 _os.path.dirname(_getattr_bytes(_bpy.data, "filepath"))
78                 if start is None else start,
79                 path[2:],
80             )
81     else:
82         if path.startswith("//"):
83             if library:
84                 start = _os.path.dirname(
85                     abspath(library.filepath))
86             return _os.path.join(
87                 _os.path.dirname(_bpy.data.filepath)
88                 if start is None else start,
89                 path[2:],
90             )
91
92     return path
93
94
95 def relpath(path, start=None):
96     """
97     Returns the path relative to the current blend file using the "//" prefix.
98
99     :arg path: An absolute path.
100     :type path: string or bytes
101     :arg start: Relative to this path,
102        when not set the current filename is used.
103     :type start: string or bytes
104     """
105     if isinstance(path, bytes):
106         if not path.startswith(b"//"):
107             if start is None:
108                 start = _os.path.dirname(_getattr_bytes(_bpy.data, "filepath"))
109             return b"//" + _os.path.relpath(path, start)
110     else:
111         if not path.startswith("//"):
112             if start is None:
113                 start = _os.path.dirname(_bpy.data.filepath)
114             return "//" + _os.path.relpath(path, start)
115
116     return path
117
118
119 def is_subdir(path, directory):
120     """
121     Returns true if *path* in a subdirectory of *directory*.
122     Both paths must be absolute.
123
124     :arg path: An absolute path.
125     :type path: string or bytes
126     """
127     from os.path import normpath, normcase, sep
128     path = normpath(normcase(path))
129     directory = normpath(normcase(directory))
130     if len(path) > len(directory):
131         sep = sep.encode('ascii') if isinstance(directory, bytes) else sep
132         if path.startswith(directory.rstrip(sep) + sep):
133             return True
134     return False
135
136
137 def clean_name(name, replace="_"):
138     """
139     Returns a name with characters replaced that
140     may cause problems under various circumstances,
141     such as writing to a file.
142     All characters besides A-Z/a-z, 0-9 are replaced with "_"
143     or the *replace* argument if defined.
144     """
145
146     if replace != "_":
147         if len(replace) != 1 or ord(replace) > 255:
148             raise ValueError("Value must be a single ascii character")
149
150     def maketrans_init():
151         trans_cache = clean_name._trans_cache
152         trans = trans_cache.get(replace)
153         if trans is None:
154             bad_chars = (
155                 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
156                 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
157                 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
158                 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
159                 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,
160                 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2e, 0x2f, 0x3a,
161                 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x5b, 0x5c,
162                 0x5d, 0x5e, 0x60, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f,
163                 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87,
164                 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f,
165                 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,
166                 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f,
167                 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7,
168                 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf,
169                 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7,
170                 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf,
171                 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7,
172                 0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf,
173                 0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7,
174                 0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf,
175                 0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7,
176                 0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef,
177                 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7,
178                 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe,
179             )
180             trans = str.maketrans({char: replace for char in bad_chars})
181             trans_cache[replace] = trans
182         return trans
183
184     trans = maketrans_init()
185     return name.translate(trans)
186
187
188 clean_name._trans_cache = {}
189
190
191 def _clean_utf8(name):
192     if type(name) == bytes:
193         return name.decode("utf8", "replace")
194     else:
195         return name.encode("utf8", "replace").decode("utf8")
196
197
198 _display_name_literals = {
199     ":": "_colon_",
200     "+": "_plus_",
201 }
202
203
204 def display_name(name, *, has_ext=True):
205     """
206     Creates a display string from name to be used menus and the user interface.
207     Capitalize the first letter in all lowercase names,
208     mixed case names are kept as is. Intended for use with
209     filenames and module names.
210     """
211
212     if has_ext:
213         name = _os.path.splitext(basename(name))[0]
214
215     # string replacements
216     for disp_value, file_value in _display_name_literals.items():
217         name = name.replace(file_value, disp_value)
218
219     # strip to allow underscore prefix
220     # (when paths can't start with numbers for eg).
221     name = name.replace("_", " ").lstrip(" ")
222
223     if name.islower():
224         name = name.lower().title()
225
226     name = _clean_utf8(name)
227     return name
228
229
230 def display_name_to_filepath(name):
231     """
232     Performs the reverse of display_name using literal versions of characters
233     which aren't supported in a filepath.
234     """
235     for disp_value, file_value in _display_name_literals.items():
236         name = name.replace(disp_value, file_value)
237     return name
238
239
240 def display_name_from_filepath(name):
241     """
242     Returns the path stripped of directory and extension,
243     ensured to be utf8 compatible.
244     """
245
246     name = _os.path.splitext(basename(name))[0]
247     name = _clean_utf8(name)
248     return name
249
250
251 def resolve_ncase(path):
252     """
253     Resolve a case insensitive path on a case sensitive system,
254     returning a string with the path if found else return the original path.
255     """
256
257     def _ncase_path_found(path):
258         if not path or _os.path.exists(path):
259             return path, True
260
261         # filename may be a directory or a file
262         filename = _os.path.basename(path)
263         dirpath = _os.path.dirname(path)
264
265         suffix = path[:0]  # "" but ensure byte/str match
266         if not filename:  # dir ends with a slash?
267             if len(dirpath) < len(path):
268                 suffix = path[:len(path) - len(dirpath)]
269
270             filename = _os.path.basename(dirpath)
271             dirpath = _os.path.dirname(dirpath)
272
273         if not _os.path.exists(dirpath):
274             if dirpath == path:
275                 return path, False
276
277             dirpath, found = _ncase_path_found(dirpath)
278
279             if not found:
280                 return path, False
281
282         # at this point, the directory exists but not the file
283
284         # we are expecting 'dirpath' to be a directory, but it could be a file
285         if _os.path.isdir(dirpath):
286             try:
287                 files = _os.listdir(dirpath)
288             except PermissionError:
289                 # We might not have the permission to list dirpath...
290                 return path, False
291         else:
292             return path, False
293
294         filename_low = filename.lower()
295         f_iter_nocase = None
296
297         for f_iter in files:
298             if f_iter.lower() == filename_low:
299                 f_iter_nocase = f_iter
300                 break
301
302         if f_iter_nocase:
303             return _os.path.join(dirpath, f_iter_nocase) + suffix, True
304         else:
305             # can't find the right one, just return the path as is.
306             return path, False
307
308     ncase_path, found = _ncase_path_found(path)
309     return ncase_path if found else path
310
311
312 def ensure_ext(filepath, ext, case_sensitive=False):
313     """
314     Return the path with the extension added if it is not already set.
315
316     :arg ext: The extension to check for, can be a compound extension. Should
317               start with a dot, such as '.blend' or '.tar.gz'.
318     :type ext: string
319     :arg case_sensitive: Check for matching case when comparing extensions.
320     :type case_sensitive: bool
321     """
322
323     if case_sensitive:
324         if filepath.endswith(ext):
325             return filepath
326     else:
327         if filepath[-len(ext):].lower().endswith(ext.lower()):
328             return filepath
329
330     return filepath + ext
331
332
333 def module_names(path, recursive=False):
334     """
335     Return a list of modules which can be imported from *path*.
336
337     :arg path: a directory to scan.
338     :type path: string
339     :arg recursive: Also return submodule names for packages.
340     :type recursive: bool
341     :return: a list of string pairs (module_name, module_file).
342     :rtype: list
343     """
344
345     from os.path import join, isfile
346
347     modules = []
348
349     for filename in sorted(_os.listdir(path)):
350         if filename == "modules":
351             pass  # XXX, hard coded exception.
352         elif filename.endswith(".py") and filename != "__init__.py":
353             fullpath = join(path, filename)
354             modules.append((filename[0:-3], fullpath))
355         elif not filename.startswith("."):
356             # Skip hidden files since they are used by for version control.
357             directory = join(path, filename)
358             fullpath = join(directory, "__init__.py")
359             if isfile(fullpath):
360                 modules.append((filename, fullpath))
361                 if recursive:
362                     for mod_name, mod_path in module_names(directory, True):
363                         modules.append(("%s.%s" % (filename, mod_name),
364                                         mod_path,
365                                         ))
366
367     return modules
368
369
370 def basename(path):
371     """
372     Equivalent to os.path.basename, but skips a "//" prefix.
373
374     Use for Windows compatibility.
375     """
376     return _os.path.basename(path[2:] if path[:2] in {"//", b"//"} else path)
377
378
379 def native_pathsep(path):
380     """
381     Replace the path separator with the systems native ``os.sep``.
382     """
383     if type(path) is str:
384         if _os.sep == "/":
385             return path.replace("\\", "/")
386         else:
387             if path.startswith("//"):
388                 return "//" + path[2:].replace("/", "\\")
389             else:
390                 return path.replace("/", "\\")
391     else:  # bytes
392         if _os.sep == "/":
393             return path.replace(b"\\", b"/")
394         else:
395             if path.startswith(b"//"):
396                 return b"//" + path[2:].replace(b"/", b"\\")
397             else:
398                 return path.replace(b"/", b"\\")
399
400
401 def reduce_dirs(dirs):
402     """
403     Given a sequence of directories, remove duplicates and
404     any directories nested in one of the other paths.
405     (Useful for recursive path searching).
406
407     :arg dirs: Sequence of directory paths.
408     :type dirs: sequence
409     :return: A unique list of paths.
410     :rtype: list
411     """
412     dirs = list({_os.path.normpath(_os.path.abspath(d)) for d in dirs})
413     dirs.sort(key=lambda d: len(d))
414     for i in range(len(dirs) - 1, -1, -1):
415         for j in range(i):
416             print(i, j)
417             if len(dirs[i]) == len(dirs[j]):
418                 break
419             elif is_subdir(dirs[i], dirs[j]):
420                 del dirs[i]
421                 break
422     return dirs